diff --git a/.coveragerc b/.coveragerc
index c3a56fa27c0..7c741dc26cd 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -115,12 +115,14 @@ omit =
homeassistant/components/bmw_connected_drive/notify.py
homeassistant/components/bmw_connected_drive/sensor.py
homeassistant/components/bosch_shc/__init__.py
- homeassistant/components/bosch_shc/const.py
homeassistant/components/bosch_shc/binary_sensor.py
+ homeassistant/components/bosch_shc/const.py
homeassistant/components/bosch_shc/entity.py
+ homeassistant/components/bosch_shc/sensor.py
homeassistant/components/braviatv/__init__.py
homeassistant/components/braviatv/const.py
homeassistant/components/braviatv/media_player.py
+ homeassistant/components/braviatv/remote.py
homeassistant/components/broadlink/__init__.py
homeassistant/components/broadlink/const.py
homeassistant/components/broadlink/remote.py
@@ -152,7 +154,7 @@ omit =
homeassistant/components/clicksend_tts/notify.py
homeassistant/components/cmus/media_player.py
homeassistant/components/co2signal/*
- homeassistant/components/coinbase/*
+ homeassistant/components/coinbase/sensor.py
homeassistant/components/comed_hourly_pricing/sensor.py
homeassistant/components/comfoconnect/fan.py
homeassistant/components/concord232/alarm_control_panel.py
@@ -182,11 +184,9 @@ omit =
homeassistant/components/denonavr/media_player.py
homeassistant/components/denonavr/receiver.py
homeassistant/components/deutsche_bahn/sensor.py
- homeassistant/components/devolo_home_control/binary_sensor.py
homeassistant/components/devolo_home_control/climate.py
homeassistant/components/devolo_home_control/const.py
homeassistant/components/devolo_home_control/cover.py
- homeassistant/components/devolo_home_control/devolo_device.py
homeassistant/components/devolo_home_control/devolo_multi_level_switch.py
homeassistant/components/devolo_home_control/light.py
homeassistant/components/devolo_home_control/sensor.py
@@ -221,6 +221,7 @@ omit =
homeassistant/components/ecobee/__init__.py
homeassistant/components/ecobee/binary_sensor.py
homeassistant/components/ecobee/climate.py
+ homeassistant/components/ecobee/humidifier.py
homeassistant/components/ecobee/notify.py
homeassistant/components/ecobee/sensor.py
homeassistant/components/ecobee/weather.py
@@ -273,6 +274,7 @@ omit =
homeassistant/components/esphome/entry_data.py
homeassistant/components/esphome/fan.py
homeassistant/components/esphome/light.py
+ homeassistant/components/esphome/number.py
homeassistant/components/esphome/sensor.py
homeassistant/components/esphome/switch.py
homeassistant/components/essent/sensor.py
@@ -341,6 +343,7 @@ omit =
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
homeassistant/components/fritzbox_callmonitor/const.py
homeassistant/components/fritzbox_callmonitor/base.py
@@ -541,15 +544,12 @@ omit =
homeassistant/components/lastfm/sensor.py
homeassistant/components/launch_library/const.py
homeassistant/components/launch_library/sensor.py
- homeassistant/components/lcn/__init__.py
homeassistant/components/lcn/binary_sensor.py
homeassistant/components/lcn/climate.py
- homeassistant/components/lcn/const.py
homeassistant/components/lcn/cover.py
homeassistant/components/lcn/helpers.py
homeassistant/components/lcn/light.py
homeassistant/components/lcn/scene.py
- homeassistant/components/lcn/schemas.py
homeassistant/components/lcn/sensor.py
homeassistant/components/lcn/services.py
homeassistant/components/lcn/switch.py
@@ -613,6 +613,7 @@ omit =
homeassistant/components/meteoalarm/*
homeassistant/components/meteoclimatic/__init__.py
homeassistant/components/meteoclimatic/const.py
+ homeassistant/components/meteoclimatic/sensor.py
homeassistant/components/meteoclimatic/weather.py
homeassistant/components/metoffice/sensor.py
homeassistant/components/metoffice/weather.py
@@ -690,7 +691,7 @@ omit =
homeassistant/components/niko_home_control/light.py
homeassistant/components/nilu/air_quality.py
homeassistant/components/nissan_leaf/*
- homeassistant/components/nmap_tracker/device_tracker.py
+ homeassistant/components/nmap_tracker/*
homeassistant/components/nmbs/sensor.py
homeassistant/components/notion/__init__.py
homeassistant/components/notion/binary_sensor.py
@@ -769,6 +770,7 @@ omit =
homeassistant/components/pcal9535a/*
homeassistant/components/pencom/switch.py
homeassistant/components/philips_js/__init__.py
+ homeassistant/components/philips_js/light.py
homeassistant/components/philips_js/media_player.py
homeassistant/components/philips_js/remote.py
homeassistant/components/pi_hole/sensor.py
@@ -846,6 +848,8 @@ omit =
homeassistant/components/ripple/sensor.py
homeassistant/components/rituals_perfume_genie/binary_sensor.py
homeassistant/components/rituals_perfume_genie/entity.py
+ homeassistant/components/rituals_perfume_genie/number.py
+ homeassistant/components/rituals_perfume_genie/select.py
homeassistant/components/rituals_perfume_genie/sensor.py
homeassistant/components/rituals_perfume_genie/switch.py
homeassistant/components/rituals_perfume_genie/__init__.py
@@ -921,9 +925,11 @@ omit =
homeassistant/components/slack/notify.py
homeassistant/components/sia/__init__.py
homeassistant/components/sia/alarm_control_panel.py
+ homeassistant/components/sia/binary_sensor.py
homeassistant/components/sia/const.py
homeassistant/components/sia/hub.py
homeassistant/components/sia/utils.py
+ homeassistant/components/sia/sia_entity_base.py
homeassistant/components/sinch/*
homeassistant/components/slide/*
homeassistant/components/sma/__init__.py
@@ -943,6 +949,7 @@ omit =
homeassistant/components/snmp/*
homeassistant/components/sochain/sensor.py
homeassistant/components/solaredge/__init__.py
+ homeassistant/components/solaredge/coordinator.py
homeassistant/components/solaredge/sensor.py
homeassistant/components/solaredge_local/sensor.py
homeassistant/components/solarlog/*
@@ -970,6 +977,7 @@ omit =
homeassistant/components/squeezebox/__init__.py
homeassistant/components/squeezebox/browse_media.py
homeassistant/components/squeezebox/media_player.py
+ homeassistant/components/ssdp/util.py
homeassistant/components/starline/*
homeassistant/components/starlingbank/sensor.py
homeassistant/components/steam_online/sensor.py
@@ -985,6 +993,7 @@ omit =
homeassistant/components/swiss_public_transport/sensor.py
homeassistant/components/swisscom/device_tracker.py
homeassistant/components/switchbot/switch.py
+ homeassistant/components/switcher_kis/sensor.py
homeassistant/components/switcher_kis/switch.py
homeassistant/components/switchmate/switch.py
homeassistant/components/syncthing/__init__.py
@@ -1206,6 +1215,7 @@ omit =
homeassistant/components/xmpp/notify.py
homeassistant/components/xs1/*
homeassistant/components/yale_smart_alarm/alarm_control_panel.py
+ homeassistant/components/yamaha_musiccast/__init__.py
homeassistant/components/yamaha_musiccast/media_player.py
homeassistant/components/yandex_transport/*
homeassistant/components/yeelightsunflower/light.py
diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml
index 607af99fb51..0c30f6887b2 100644
--- a/.github/workflows/builder.yml
+++ b/.github/workflows/builder.yml
@@ -102,13 +102,13 @@ jobs:
version="$(python setup.py -V)"
- name: Login to DockerHub
- uses: docker/login-action@v1.9.0
+ uses: docker/login-action@v1.10.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
- uses: docker/login-action@v1.9.0
+ uses: docker/login-action@v1.10.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -154,13 +154,13 @@ jobs:
uses: actions/checkout@v2.3.4
- name: Login to DockerHub
- uses: docker/login-action@v1.9.0
+ uses: docker/login-action@v1.10.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
- uses: docker/login-action@v1.9.0
+ uses: docker/login-action@v1.10.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -217,13 +217,13 @@ jobs:
uses: actions/checkout@v2.3.4
- name: Login to DockerHub
- uses: docker/login-action@v1.9.0
+ uses: docker/login-action@v1.10.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
- uses: docker/login-action@v1.9.0
+ uses: docker/login-action@v1.10.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -307,5 +307,9 @@ jobs:
create_manifest "${docker_reg}" "latest" "${{ needs.init.outputs.version }}"
create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}"
create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}"
+
+ # Create series version tag (e.g. 2021.6)
+ v="${{ needs.init.outputs.version }}"
+ create_manifest "${docker_reg}" "${v%.*}" "${{ needs.init.outputs.version }}"
fi
done
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index fe14e44beed..0809cf604cf 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -10,7 +10,7 @@ on:
pull_request: ~
env:
- CACHE_VERSION: 1
+ CACHE_VERSION: 2
DEFAULT_PYTHON: 3.8
PRE_COMMIT_CACHE: ~/.cache/pre-commit
SQLALCHEMY_WARN_20: 1
@@ -41,7 +41,7 @@ jobs:
hashFiles('homeassistant/package_constraints.txt') }}"
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: >-
@@ -65,7 +65,7 @@ jobs:
hashFiles('.pre-commit-config.yaml') }}"
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: ${{ env.PRE_COMMIT_CACHE }}
key: >-
@@ -92,7 +92,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@@ -104,7 +104,7 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@@ -132,7 +132,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@@ -144,7 +144,7 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@@ -172,7 +172,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@@ -184,7 +184,7 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@@ -234,7 +234,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@@ -246,7 +246,7 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@@ -277,7 +277,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@@ -289,7 +289,7 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@@ -320,7 +320,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@@ -332,7 +332,7 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@@ -360,7 +360,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@@ -372,7 +372,7 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@@ -403,7 +403,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@@ -415,7 +415,7 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@@ -454,7 +454,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@@ -466,7 +466,7 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@@ -496,7 +496,7 @@ jobs:
uses: actions/checkout@v2.3.4
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ matrix.python-version }}-${{
@@ -525,7 +525,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@@ -561,7 +561,7 @@ jobs:
hashFiles('homeassistant/package_constraints.txt') }}"
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: >-
@@ -598,7 +598,7 @@ jobs:
uses: actions/checkout@v2.3.4
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ matrix.python-version }}-${{
@@ -629,7 +629,7 @@ jobs:
uses: actions/checkout@v2.3.4
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ matrix.python-version }}-${{
@@ -663,7 +663,7 @@ jobs:
uses: actions/checkout@v2.3.4
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ matrix.python-version }}-${{
@@ -700,7 +700,7 @@ jobs:
-p no:sugar \
tests
- name: Upload coverage artifact
- uses: actions/upload-artifact@v2.2.3
+ uses: actions/upload-artifact@v2.2.4
with:
name: coverage-${{ matrix.python-version }}-group${{ matrix.group }}
path: .coverage
@@ -721,7 +721,7 @@ jobs:
uses: actions/checkout@v2.3.4
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache@v2.1.5
+ uses: actions/cache@v2.1.6
with:
path: venv
key: ${{ runner.os }}-${{ matrix.python-version }}-${{
@@ -740,4 +740,4 @@ jobs:
coverage report --fail-under=94
coverage xml
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v1.5.0
+ uses: codecov/codecov-action@v1.5.2
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index ab506274585..8e97c61194b 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -45,13 +45,13 @@ jobs:
) > .env_file
- name: Upload env_file
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v2.2.4
with:
name: env_file
path: ./.env_file
- name: Upload requirements_diff
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v2.2.4
with:
name: requirements_diff
path: ./requirements_diff.txt
@@ -65,7 +65,6 @@ jobs:
matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
tag:
- - "3.8-alpine3.12"
- "3.9-alpine3.13"
steps:
- name: Checkout the repository
@@ -82,7 +81,7 @@ jobs:
name: requirements_diff
- name: Build wheels
- uses: home-assistant/wheels@2021.05.4
+ uses: home-assistant/wheels@2021.06.0
with:
tag: ${{ matrix.tag }}
arch: ${{ matrix.arch }}
@@ -106,7 +105,6 @@ jobs:
matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
tag:
- - "3.8-alpine3.12"
- "3.9-alpine3.13"
steps:
- name: Checkout the repository
@@ -152,7 +150,7 @@ jobs:
done
- name: Build wheels
- uses: home-assistant/wheels@2021.05.4
+ uses: home-assistant/wheels@2021.06.0
with:
tag: ${{ matrix.tag }}
arch: ${{ matrix.arch }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9ead1fd09bb..31d5e9dd16c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,7 +5,7 @@ repos:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/psf/black
- rev: 21.5b1
+ rev: 21.6b0
hooks:
- id: black
args:
diff --git a/.strict-typing b/.strict-typing
index 00bc3447d22..09578153163 100644
--- a/.strict-typing
+++ b/.strict-typing
@@ -12,6 +12,7 @@ homeassistant.components.airly.*
homeassistant.components.aladdin_connect.*
homeassistant.components.alarm_control_panel.*
homeassistant.components.amazon_polly.*
+homeassistant.components.ambee.*
homeassistant.components.ampio.*
homeassistant.components.automation.*
homeassistant.components.binary_sensor.*
@@ -24,15 +25,19 @@ homeassistant.components.canary.*
homeassistant.components.cover.*
homeassistant.components.device_automation.*
homeassistant.components.device_tracker.*
+homeassistant.components.dnsip.*
+homeassistant.components.dsmr.*
homeassistant.components.dunehd.*
homeassistant.components.elgato.*
homeassistant.components.fitbit.*
+homeassistant.components.forecast_solar.*
homeassistant.components.fritzbox.*
homeassistant.components.frontend.*
homeassistant.components.geo_location.*
homeassistant.components.gios.*
homeassistant.components.group.*
homeassistant.components.history.*
+homeassistant.components.homeassistant.triggers.event
homeassistant.components.http.*
homeassistant.components.huawei_lte.*
homeassistant.components.hyperion.*
@@ -41,24 +46,31 @@ homeassistant.components.integration.*
homeassistant.components.knx.*
homeassistant.components.kraken.*
homeassistant.components.light.*
+homeassistant.components.local_ip.*
homeassistant.components.lock.*
homeassistant.components.mailbox.*
homeassistant.components.media_player.*
+homeassistant.components.mysensors.*
homeassistant.components.nam.*
homeassistant.components.network.*
+homeassistant.components.no_ip.*
homeassistant.components.notify.*
homeassistant.components.number.*
homeassistant.components.onewire.*
homeassistant.components.persistent_notification.*
+homeassistant.components.pi_hole.*
homeassistant.components.proximity.*
homeassistant.components.recorder.purge
homeassistant.components.recorder.repack
homeassistant.components.recorder.statistics
homeassistant.components.remote.*
homeassistant.components.scene.*
+homeassistant.components.select.*
homeassistant.components.sensor.*
homeassistant.components.slack.*
homeassistant.components.sonos.media_player
+homeassistant.components.ssdp.*
+homeassistant.components.stream.*
homeassistant.components.sun.*
homeassistant.components.switch.*
homeassistant.components.synology_dsm.*
@@ -66,10 +78,12 @@ homeassistant.components.systemmonitor.*
homeassistant.components.tcp.*
homeassistant.components.tts.*
homeassistant.components.upcloud.*
+homeassistant.components.uptime.*
homeassistant.components.vacuum.*
homeassistant.components.water_heater.*
homeassistant.components.weather.*
homeassistant.components.websocket_api.*
+homeassistant.components.zodiac.*
homeassistant.components.zeroconf.*
homeassistant.components.zone.*
homeassistant.components.zwave_js.*
diff --git a/CODEOWNERS b/CODEOWNERS
index 2bee90dcf99..c651e35dcc3 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -33,6 +33,7 @@ homeassistant/components/alarmdecoder/* @ajschmidt8
homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy
homeassistant/components/almond/* @gcampax @balloob
homeassistant/components/alpha_vantage/* @fabaff
+homeassistant/components/ambee/* @frenck
homeassistant/components/ambiclimate/* @danielhiversen
homeassistant/components/ambient_station/* @bachya
homeassistant/components/analytics/* @home-assistant/core @ludeeus
@@ -71,7 +72,7 @@ homeassistant/components/bmp280/* @belidzs
homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe
homeassistant/components/bond/* @prystupa
homeassistant/components/bosch_shc/* @tschamm
-homeassistant/components/braviatv/* @bieniu
+homeassistant/components/braviatv/* @bieniu @Drafteed
homeassistant/components/broadlink/* @danielhiversen @felipediel
homeassistant/components/brother/* @bieniu
homeassistant/components/brunt/* @eavanvalkenburg
@@ -87,6 +88,7 @@ homeassistant/components/cisco_webex_teams/* @fbradyirl
homeassistant/components/climacell/* @raman325
homeassistant/components/cloud/* @home-assistant/cloud
homeassistant/components/cloudflare/* @ludeeus @ctalkington
+homeassistant/components/coinbase/* @tombrien
homeassistant/components/color_extractor/* @GenericStudent
homeassistant/components/comfoconnect/* @michaelarnauts
homeassistant/components/compensation/* @Petro31
@@ -117,7 +119,7 @@ homeassistant/components/digital_ocean/* @fabaff
homeassistant/components/directv/* @ctalkington
homeassistant/components/discogs/* @thibmaek
homeassistant/components/doorbird/* @oblogic7 @bdraco
-homeassistant/components/dsmr/* @Robbie1221
+homeassistant/components/dsmr/* @Robbie1221 @frenck
homeassistant/components/dsmr_reader/* @depl0y
homeassistant/components/dunehd/* @bieniu
homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95
@@ -146,7 +148,7 @@ homeassistant/components/ephember/* @ttroy50
homeassistant/components/epson/* @pszafer
homeassistant/components/epsonworkforce/* @ThaStealth
homeassistant/components/eq3btsmart/* @rytilahti
-homeassistant/components/esphome/* @OttoWinter
+homeassistant/components/esphome/* @OttoWinter @jesserockz
homeassistant/components/essent/* @TheLastProject
homeassistant/components/evohome/* @zxdavb
homeassistant/components/ezviz/* @RenierM26 @baqs
@@ -162,10 +164,12 @@ homeassistant/components/flo/* @dmulcahey
homeassistant/components/flock/* @fabaff
homeassistant/components/flume/* @ChrisMandich @bdraco
homeassistant/components/flunearyou/* @bachya
+homeassistant/components/forecast_solar/* @klaasnicolaas @frenck
homeassistant/components/forked_daapd/* @uvjustin
homeassistant/components/fortios/* @kimfrellsen
homeassistant/components/foscam/* @skgsergio
homeassistant/components/freebox/* @hacf-fr @Quentame
+homeassistant/components/freedompro/* @stefano055415
homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74
homeassistant/components/fritzbox/* @mib1185
homeassistant/components/fronius/* @nielstron
@@ -300,6 +304,7 @@ homeassistant/components/minecraft_server/* @elmurato
homeassistant/components/minio/* @tkislan
homeassistant/components/mobile_app/* @robbiet480
homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
+homeassistant/components/modern_forms/* @wonderslug
homeassistant/components/monoprice/* @etsinko @OnFreund
homeassistant/components/moon/* @fabaff
homeassistant/components/motion_blinds/* @starkillerOG
@@ -421,12 +426,12 @@ homeassistant/components/scrape/* @fabaff
homeassistant/components/screenlogic/* @dieselrabbit
homeassistant/components/script/* @home-assistant/core
homeassistant/components/search/* @home-assistant/core
+homeassistant/components/select/* @home-assistant/core
homeassistant/components/sense/* @kbickar
homeassistant/components/sensibo/* @andrey-git
homeassistant/components/sentry/* @dcramer @frenck
homeassistant/components/serial/* @fabaff
homeassistant/components/seven_segments/* @fabaff
-homeassistant/components/seventeentrack/* @bachya
homeassistant/components/sharkiq/* @ajmarks
homeassistant/components/shell_command/* @home-assistant/core
homeassistant/components/shelly/* @balloob @bieniu @thecode @chemelli74
@@ -477,11 +482,11 @@ homeassistant/components/subaru/* @G-Two
homeassistant/components/suez_water/* @ooii
homeassistant/components/sun/* @Swamp-Ig
homeassistant/components/supla/* @mwegrzynek
-homeassistant/components/surepetcare/* @benleb
+homeassistant/components/surepetcare/* @benleb @danielhiversen
homeassistant/components/swiss_hydrological_data/* @fabaff
homeassistant/components/swiss_public_transport/* @fabaff
homeassistant/components/switchbot/* @danielhiversen
-homeassistant/components/switcher_kis/* @tomerfi
+homeassistant/components/switcher_kis/* @tomerfi @thecode
homeassistant/components/switchmate/* @danielhiversen
homeassistant/components/syncthing/* @zhulik
homeassistant/components/syncthru/* @nielstron
@@ -566,7 +571,7 @@ homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG
homeassistant/components/xiaomi_tv/* @simse
homeassistant/components/xmpp/* @fabaff @flowolf
homeassistant/components/yale_smart_alarm/* @gjohansson-ST
-homeassistant/components/yamaha_musiccast/* @jalmeroth
+homeassistant/components/yamaha_musiccast/* @vigonotion @micha91
homeassistant/components/yandex_transport/* @rishatik92 @devbis
homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn
homeassistant/components/yeelightsunflower/* @lindsaymarkward
diff --git a/build.json b/build.json
index c3e5d83dc78..c3a3eec0bee 100644
--- a/build.json
+++ b/build.json
@@ -2,11 +2,11 @@
"image": "homeassistant/{arch}-homeassistant",
"shadow_repository": "ghcr.io/home-assistant",
"build_from": {
- "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.05.0",
- "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.05.0",
- "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.05.0",
- "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.05.0",
- "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.05.0"
+ "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.06.2",
+ "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.06.2",
+ "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.06.2",
+ "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.06.2",
+ "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.06.2"
},
"labels": {
"io.hass.type": "core",
diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py
index fd2014667f8..7b609f371ef 100644
--- a/homeassistant/auth/providers/trusted_networks.py
+++ b/homeassistant/auth/providers/trusted_networks.py
@@ -81,6 +81,17 @@ class TrustedNetworksAuthProvider(AuthProvider):
"""Return trusted users per network."""
return cast(Dict[IPNetwork, Any], self.config[CONF_TRUSTED_USERS])
+ @property
+ def trusted_proxies(self) -> list[IPNetwork]:
+ """Return trusted proxies in the system."""
+ if not self.hass.http:
+ return []
+
+ return [
+ ip_network(trusted_proxy)
+ for trusted_proxy in self.hass.http.trusted_proxies
+ ]
+
@property
def support_mfa(self) -> bool:
"""Trusted Networks auth provider does not support MFA."""
@@ -178,6 +189,9 @@ class TrustedNetworksAuthProvider(AuthProvider):
):
raise InvalidAuthError("Not in trusted_networks")
+ if any(ip_addr in trusted_proxy for trusted_proxy in self.trusted_proxies):
+ raise InvalidAuthError("Can't allow access from a proxy server")
+
@callback
def async_validate_refresh_token(
self, refresh_token: RefreshToken, remote_ip: str | None = None
diff --git a/homeassistant/components/abode/translations/he.json b/homeassistant/components/abode/translations/he.json
index 6f4191da70d..17717573b68 100644
--- a/homeassistant/components/abode/translations/he.json
+++ b/homeassistant/components/abode/translations/he.json
@@ -1,10 +1,26 @@
{
"config": {
+ "abort": {
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7",
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea."
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
"step": {
+ "reauth_confirm": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05d3\u05d5\u05d0\"\u05dc"
+ }
+ },
"user": {
"data": {
- "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
- }
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05d3\u05d5\u05d0\"\u05dc"
+ },
+ "title": "\u05d9\u05e9 \u05dc\u05de\u05dc\u05d0 \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05db\u05e0\u05d9\u05e1\u05d4 \u05e9\u05dc\u05da \u05dc\u05d0\u05d3\u05d5\u05d1\u05d9"
}
}
}
diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py
index 18a4bd2dce4..a2b428cf597 100644
--- a/homeassistant/components/accuweather/__init__.py
+++ b/homeassistant/components/accuweather/__init__.py
@@ -16,13 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-from .const import (
- ATTR_FORECAST,
- CONF_FORECAST,
- COORDINATOR,
- DOMAIN,
- UNDO_UPDATE_LISTENER,
-)
+from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -45,12 +39,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
await coordinator.async_config_entry_first_refresh()
- undo_listener = entry.add_update_listener(update_listener)
+ entry.async_on_unload(entry.add_update_listener(update_listener))
- hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
- COORDINATOR: coordinator,
- UNDO_UPDATE_LISTENER: undo_listener,
- }
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
@@ -61,8 +52,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
- hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
-
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py
index 54d9b631ade..aea394446ad 100644
--- a/homeassistant/components/accuweather/const.py
+++ b/homeassistant/components/accuweather/const.py
@@ -3,6 +3,7 @@ from __future__ import annotations
from typing import Final
+from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY,
@@ -48,12 +49,10 @@ ATTR_LABEL: Final = "label"
ATTR_UNIT_IMPERIAL: Final = "unit_imperial"
ATTR_UNIT_METRIC: Final = "unit_metric"
CONF_FORECAST: Final = "forecast"
-COORDINATOR: Final = "coordinator"
DOMAIN: Final = "accuweather"
MANUFACTURER: Final = "AccuWeather, Inc."
MAX_FORECAST_DAYS: Final = 4
NAME: Final = "AccuWeather"
-UNDO_UPDATE_LISTENER: Final = "undo_update_listener"
CONDITION_CLASSES: Final[dict[str, list[int]]] = {
ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37],
@@ -235,6 +234,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: False,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"Ceiling": {
ATTR_DEVICE_CLASS: None,
@@ -243,6 +243,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_UNIT_METRIC: LENGTH_METERS,
ATTR_UNIT_IMPERIAL: LENGTH_FEET,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"CloudCover": {
ATTR_DEVICE_CLASS: None,
@@ -251,6 +252,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_UNIT_METRIC: PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE,
ATTR_ENABLED: False,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"DewPoint": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@@ -259,6 +261,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: False,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"RealFeelTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@@ -267,6 +270,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"RealFeelTemperatureShade": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@@ -275,6 +279,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: False,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"Precipitation": {
ATTR_DEVICE_CLASS: None,
@@ -283,6 +288,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_UNIT_METRIC: LENGTH_MILLIMETERS,
ATTR_UNIT_IMPERIAL: LENGTH_INCHES,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"PressureTendency": {
ATTR_DEVICE_CLASS: "accuweather__pressure_tendency",
@@ -299,6 +305,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_UNIT_METRIC: UV_INDEX,
ATTR_UNIT_IMPERIAL: UV_INDEX,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"WetBulbTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@@ -307,6 +314,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: False,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"WindChillTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@@ -315,6 +323,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: False,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"Wind": {
ATTR_DEVICE_CLASS: None,
@@ -323,6 +332,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"WindGust": {
ATTR_DEVICE_CLASS: None,
@@ -331,5 +341,6 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
ATTR_ENABLED: False,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
}
diff --git a/homeassistant/components/accuweather/model.py b/homeassistant/components/accuweather/model.py
index cc51efbd0e2..2127629728b 100644
--- a/homeassistant/components/accuweather/model.py
+++ b/homeassistant/components/accuweather/model.py
@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TypedDict
-class SensorDescription(TypedDict):
+class SensorDescription(TypedDict, total=False):
"""Sensor description class."""
device_class: str | None
@@ -13,3 +13,4 @@ class SensorDescription(TypedDict):
unit_metric: str | None
unit_imperial: str | None
enabled: bool
+ state_class: str | None
diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py
index d6f9339409f..ba99df14d9e 100644
--- a/homeassistant/components/accuweather/sensor.py
+++ b/homeassistant/components/accuweather/sensor.py
@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any, cast
-from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
@@ -28,7 +28,6 @@ from .const import (
ATTR_UNIT_IMPERIAL,
ATTR_UNIT_METRIC,
ATTRIBUTION,
- COORDINATOR,
DOMAIN,
FORECAST_SENSOR_TYPES,
MANUFACTURER,
@@ -46,9 +45,7 @@ async def async_setup_entry(
"""Add AccuWeather entities from a config_entry."""
name: str = entry.data[CONF_NAME]
- coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
- COORDINATOR
- ]
+ coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
sensors: list[AccuWeatherSensor] = []
for sensor in SENSOR_TYPES:
@@ -92,6 +89,7 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
self._device_class = None
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
self.forecast_day = forecast_day
+ self._attr_state_class = self._description.get(ATTR_STATE_CLASS)
@property
def name(self) -> str:
diff --git a/homeassistant/components/accuweather/system_health.py b/homeassistant/components/accuweather/system_health.py
index 5feed5c1f34..df1e607d15d 100644
--- a/homeassistant/components/accuweather/system_health.py
+++ b/homeassistant/components/accuweather/system_health.py
@@ -8,7 +8,7 @@ from accuweather.const import ENDPOINT
from homeassistant.components import system_health
from homeassistant.core import HomeAssistant, callback
-from .const import COORDINATOR, DOMAIN
+from .const import DOMAIN
@callback
@@ -21,8 +21,8 @@ def async_register(
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"""Get info for the info page."""
- remaining_requests = list(hass.data[DOMAIN].values())[0][
- COORDINATOR
+ remaining_requests = list(hass.data[DOMAIN].values())[
+ 0
].accuweather.requests_remaining
return {
diff --git a/homeassistant/components/accuweather/translations/he.json b/homeassistant/components/accuweather/translations/he.json
index 4c49313d977..869e00ca064 100644
--- a/homeassistant/components/accuweather/translations/he.json
+++ b/homeassistant/components/accuweather/translations/he.json
@@ -1,9 +1,19 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea."
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
"step": {
"user": {
"data": {
- "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
+ "name": "\u05e9\u05dd"
}
}
}
diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py
index 0dc4c7e270c..9a2ba769a82 100644
--- a/homeassistant/components/accuweather/weather.py
+++ b/homeassistant/components/accuweather/weather.py
@@ -13,6 +13,7 @@ from homeassistant.components.weather import (
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
+ Forecast,
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
@@ -30,7 +31,6 @@ from .const import (
ATTR_FORECAST,
ATTRIBUTION,
CONDITION_CLASSES,
- COORDINATOR,
DOMAIN,
MANUFACTURER,
NAME,
@@ -45,9 +45,7 @@ async def async_setup_entry(
"""Add a AccuWeather weather entity from a config_entry."""
name: str = entry.data[CONF_NAME]
- coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
- COORDINATOR
- ]
+ coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([AccuWeatherEntity(name, coordinator)])
@@ -156,12 +154,12 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
return None
@property
- def forecast(self) -> list[dict[str, Any]] | None:
+ def forecast(self) -> list[Forecast] | None:
"""Return the forecast array."""
if not self.coordinator.forecast:
return None
# remap keys from library to keys understood by the weather component
- forecast = [
+ return [
{
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
ATTR_FORECAST_TEMP: item["TemperatureMax"]["Value"],
@@ -183,7 +181,6 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
}
for item in self.coordinator.data[ATTR_FORECAST]
]
- return forecast
@staticmethod
def _calc_precipitation(day: dict[str, Any]) -> float:
diff --git a/homeassistant/components/acmeda/translations/he.json b/homeassistant/components/acmeda/translations/he.json
new file mode 100644
index 00000000000..498f322a7b0
--- /dev/null
+++ b/homeassistant/components/acmeda/translations/he.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "\u05de\u05d6\u05d4\u05d4 \u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/translations/he.json b/homeassistant/components/adguard/translations/he.json
index 1471fd6603b..e2114d19d97 100644
--- a/homeassistant/components/adguard/translations/he.json
+++ b/homeassistant/components/adguard/translations/he.json
@@ -1,11 +1,20 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
"step": {
"user": {
"data": {
- "host": "Host",
+ "host": "\u05de\u05d0\u05e8\u05d7",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
- "port": "\u05e4\u05d5\u05e8\u05d8"
+ "port": "\u05e4\u05d5\u05e8\u05d8",
+ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
}
}
}
diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py
index f7b295c9634..fba90148788 100644
--- a/homeassistant/components/advantage_air/binary_sensor.py
+++ b/homeassistant/components/advantage_air/binary_sensor.py
@@ -33,6 +33,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity):
"""Advantage Air Filter."""
+ _attr_device_class = DEVICE_CLASS_PROBLEM
+
@property
def name(self):
"""Return the name."""
@@ -43,11 +45,6 @@ class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity):
"""Return a unique id."""
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-filter'
- @property
- def device_class(self):
- """Return the device class of the vent."""
- return DEVICE_CLASS_PROBLEM
-
@property
def is_on(self):
"""Return if filter needs cleaning."""
@@ -57,6 +54,8 @@ class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity):
class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity):
"""Advantage Air Zone Motion."""
+ _attr_device_class = DEVICE_CLASS_MOTION
+
@property
def name(self):
"""Return the name."""
@@ -67,11 +66,6 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity):
"""Return a unique id."""
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-motion'
- @property
- def device_class(self):
- """Return the device class of the vent."""
- return DEVICE_CLASS_MOTION
-
@property
def is_on(self):
"""Return if motion is detect."""
@@ -81,6 +75,8 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity):
class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity):
"""Advantage Air Zone MyZone."""
+ _attr_entity_registry_enabled_default = False
+
@property
def name(self):
"""Return the name."""
@@ -95,8 +91,3 @@ class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity):
def is_on(self):
"""Return if this zone is the myZone."""
return self._zone["number"] == self._ac["myZone"]
-
- @property
- def entity_registry_enabled_default(self):
- """Return false to disable this entity by default."""
- return False
diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py
index 60caf15be25..d890fa43207 100644
--- a/homeassistant/components/advantage_air/climate.py
+++ b/homeassistant/components/advantage_air/climate.py
@@ -6,6 +6,7 @@ from homeassistant.components.climate.const import (
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
+ HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
HVAC_MODE_FAN_ONLY,
@@ -31,9 +32,18 @@ ADVANTAGE_AIR_HVAC_MODES = {
"cool": HVAC_MODE_COOL,
"vent": HVAC_MODE_FAN_ONLY,
"dry": HVAC_MODE_DRY,
+ "myauto": HVAC_MODE_AUTO,
}
HASS_HVAC_MODES = {v: k for k, v in ADVANTAGE_AIR_HVAC_MODES.items()}
+AC_HVAC_MODES = [
+ HVAC_MODE_OFF,
+ HVAC_MODE_COOL,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_FAN_ONLY,
+ HVAC_MODE_DRY,
+]
+
ADVANTAGE_AIR_FAN_MODES = {
"auto": FAN_AUTO,
"low": FAN_LOW,
@@ -43,13 +53,6 @@ ADVANTAGE_AIR_FAN_MODES = {
HASS_FAN_MODES = {v: k for k, v in ADVANTAGE_AIR_FAN_MODES.items()}
FAN_SPEEDS = {FAN_LOW: 30, FAN_MEDIUM: 60, FAN_HIGH: 100}
-AC_HVAC_MODES = [
- HVAC_MODE_OFF,
- HVAC_MODE_COOL,
- HVAC_MODE_HEAT,
- HVAC_MODE_FAN_ONLY,
- HVAC_MODE_DRY,
-]
ADVANTAGE_AIR_SERVICE_SET_MYZONE = "set_myzone"
ZONE_HVAC_MODES = [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY]
@@ -130,6 +133,8 @@ class AdvantageAirAC(AdvantageAirClimateEntity):
@property
def hvac_modes(self):
"""Return the supported HVAC modes."""
+ if self._ac.get("myAutoModeEnabled"):
+ return AC_HVAC_MODES + [HVAC_MODE_AUTO]
return AC_HVAC_MODES
@property
diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py
index 8f027b1bdaf..8c6834ac76e 100644
--- a/homeassistant/components/advantage_air/sensor.py
+++ b/homeassistant/components/advantage_air/sensor.py
@@ -44,6 +44,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air timer control."""
+ _attr_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT
+
def __init__(self, instance, ac_key, action):
"""Initialize the Advantage Air timer control."""
super().__init__(instance, ac_key)
@@ -65,11 +67,6 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity):
"""Return the current value."""
return self._ac[self._time_key]
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return ADVANTAGE_AIR_SET_COUNTDOWN_UNIT
-
@property
def icon(self):
"""Return a representative icon of the timer."""
@@ -86,6 +83,8 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity):
class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air Zone Vent Sensor."""
+ _attr_unit_of_measurement = PERCENTAGE
+
@property
def name(self):
"""Return the name."""
@@ -103,11 +102,6 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity):
return self._zone["value"]
return 0
- @property
- def unit_of_measurement(self):
- """Return the percent sign."""
- return PERCENTAGE
-
@property
def icon(self):
"""Return a representative icon."""
@@ -119,6 +113,8 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity):
class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air Zone wireless signal sensor."""
+ _attr_unit_of_measurement = PERCENTAGE
+
@property
def name(self):
"""Return the name."""
@@ -134,11 +130,6 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
"""Return the current value of the wireless signal."""
return self._zone["rssi"]
- @property
- def unit_of_measurement(self):
- """Return the percent sign."""
- return PERCENTAGE
-
@property
def icon(self):
"""Return a representative icon."""
diff --git a/homeassistant/components/advantage_air/translations/he.json b/homeassistant/components/advantage_air/translations/he.json
new file mode 100644
index 00000000000..7c534baa977
--- /dev/null
+++ b/homeassistant/components/advantage_air/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"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ },
+ "description": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc- API \u05e9\u05dc \u05d4\u05d8\u05d0\u05d1\u05dc\u05d8 \u05e9\u05dc\u05da \u05d4\u05de\u05d5\u05ea\u05e7\u05df \u05e2\u05dc \u05d4\u05e7\u05d9\u05e8.",
+ "title": "\u05d4\u05ea\u05d7\u05d1\u05e8"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py
index 879f59fa2fc..7a20e77f0b0 100644
--- a/homeassistant/components/aemet/__init__.py
+++ b/homeassistant/components/aemet/__init__.py
@@ -19,13 +19,13 @@ from .weather_update_coordinator import WeatherUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up AEMET OpenData as config entry."""
- name = config_entry.data[CONF_NAME]
- api_key = config_entry.data[CONF_API_KEY]
- latitude = config_entry.data[CONF_LATITUDE]
- longitude = config_entry.data[CONF_LONGITUDE]
- station_updates = config_entry.options.get(CONF_STATION_UPDATES, True)
+ name = entry.data[CONF_NAME]
+ api_key = entry.data[CONF_API_KEY]
+ latitude = entry.data[CONF_LATITUDE]
+ longitude = entry.data[CONF_LONGITUDE]
+ station_updates = entry.options.get(CONF_STATION_UPDATES, True)
aemet = AEMET(api_key)
weather_coordinator = WeatherUpdateCoordinator(
@@ -35,30 +35,28 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
await weather_coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
- hass.data[DOMAIN][config_entry.entry_id] = {
+ hass.data[DOMAIN][entry.entry_id] = {
ENTRY_NAME: name,
ENTRY_WEATHER_COORDINATOR: weather_coordinator,
}
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
- config_entry.async_on_unload(config_entry.add_update_listener(async_update_options))
+ entry.async_on_unload(entry.add_update_listener(async_update_options))
return True
-async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
+async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update options."""
- await hass.config_entries.async_reload(config_entry.entry_id)
+ await hass.config_entries.async_reload(entry.entry_id)
-async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
- unload_ok = await hass.config_entries.async_unload_platforms(
- config_entry, PLATFORMS
- )
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
- hass.data[DOMAIN].pop(config_entry.entry_id)
+ hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
diff --git a/homeassistant/components/aemet/translations/de.json b/homeassistant/components/aemet/translations/de.json
index d5312805722..2a4a927b90a 100644
--- a/homeassistant/components/aemet/translations/de.json
+++ b/homeassistant/components/aemet/translations/de.json
@@ -18,5 +18,14 @@
"title": "[void]"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "station_updates": "Sammeln von Daten von AEMET-Wetterstationen"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/he.json b/homeassistant/components/aemet/translations/he.json
new file mode 100644
index 00000000000..5a7d693afec
--- /dev/null
+++ b/homeassistant/components/aemet/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/pl.json b/homeassistant/components/aemet/translations/pl.json
index 2c5c24fae2a..8531ca47fd6 100644
--- a/homeassistant/components/aemet/translations/pl.json
+++ b/homeassistant/components/aemet/translations/pl.json
@@ -18,5 +18,14 @@
"title": "AEMET OpenData"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "station_updates": "Zbieraj dane ze stacji pogodowych AEMET"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/agent_dvr/translations/he.json b/homeassistant/components/agent_dvr/translations/he.json
index 6268822a90a..e50d45e5608 100644
--- a/homeassistant/components/agent_dvr/translations/he.json
+++ b/homeassistant/components/agent_dvr/translations/he.json
@@ -3,10 +3,14 @@
"abort": {
"already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
},
+ "error": {
+ "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
"step": {
"user": {
"data": {
- "host": "Host",
+ "host": "\u05de\u05d0\u05e8\u05d7",
"port": "\u05e4\u05d5\u05e8\u05d8"
}
}
diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py
index 58899d76ef8..0304945e6d2 100644
--- a/homeassistant/components/airly/__init__.py
+++ b/homeassistant/components/airly/__init__.py
@@ -11,9 +11,11 @@ from airly import Airly
from airly.exceptions import AirlyError
import async_timeout
+from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
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 import entity_registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import async_get_registry
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -31,7 +33,7 @@ from .const import (
NO_AIRLY_SENSORS,
)
-PLATFORMS = ["air_quality", "sensor"]
+PLATFORMS = ["sensor"]
_LOGGER = logging.getLogger(__name__)
@@ -111,6 +113,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+ # Remove air_quality entities from registry if they exist
+ ent_reg = entity_registry.async_get(hass)
+ unique_id = f"{coordinator.latitude}-{coordinator.longitude}"
+ if entity_id := ent_reg.async_get_entity_id(
+ AIR_QUALITY_PLATFORM, DOMAIN, unique_id
+ ):
+ _LOGGER.debug("Removing deprecated air_quality entity %s", entity_id)
+ ent_reg.async_remove(entity_id)
+
return True
diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py
deleted file mode 100644
index 337d3a723fa..00000000000
--- a/homeassistant/components/airly/air_quality.py
+++ /dev/null
@@ -1,143 +0,0 @@
-"""Support for the Airly air_quality service."""
-from __future__ import annotations
-
-from typing import Any
-
-from homeassistant.components.air_quality import (
- ATTR_AQI,
- ATTR_PM_2_5,
- ATTR_PM_10,
- AirQualityEntity,
-)
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_NAME
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity import DeviceInfo
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from . import AirlyDataUpdateCoordinator
-from .const import (
- ATTR_API_ADVICE,
- ATTR_API_CAQI,
- ATTR_API_CAQI_DESCRIPTION,
- ATTR_API_CAQI_LEVEL,
- ATTR_API_PM10,
- ATTR_API_PM10_LIMIT,
- ATTR_API_PM10_PERCENT,
- ATTR_API_PM25,
- ATTR_API_PM25_LIMIT,
- ATTR_API_PM25_PERCENT,
- ATTRIBUTION,
- DEFAULT_NAME,
- DOMAIN,
- LABEL_ADVICE,
- MANUFACTURER,
-)
-
-LABEL_AQI_DESCRIPTION = f"{ATTR_AQI}_description"
-LABEL_AQI_LEVEL = f"{ATTR_AQI}_level"
-LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit"
-LABEL_PM_2_5_PERCENT = f"{ATTR_PM_2_5}_percent_of_limit"
-LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit"
-LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit"
-
-PARALLEL_UPDATES = 1
-
-
-async def async_setup_entry(
- hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
-) -> None:
- """Set up Airly air_quality entity based on a config entry."""
- name = entry.data[CONF_NAME]
-
- coordinator = hass.data[DOMAIN][entry.entry_id]
-
- async_add_entities([AirlyAirQuality(coordinator, name)], False)
-
-
-class AirlyAirQuality(CoordinatorEntity, AirQualityEntity):
- """Define an Airly air quality."""
-
- coordinator: AirlyDataUpdateCoordinator
-
- def __init__(self, coordinator: AirlyDataUpdateCoordinator, name: str) -> None:
- """Initialize."""
- super().__init__(coordinator)
- self._name = name
- self._icon = "mdi:blur"
-
- @property
- def name(self) -> str:
- """Return the name."""
- return self._name
-
- @property
- def icon(self) -> str:
- """Return the icon."""
- return self._icon
-
- @property
- def air_quality_index(self) -> float | None:
- """Return the air quality index."""
- return round_state(self.coordinator.data[ATTR_API_CAQI])
-
- @property
- def particulate_matter_2_5(self) -> float | None:
- """Return the particulate matter 2.5 level."""
- return round_state(self.coordinator.data.get(ATTR_API_PM25))
-
- @property
- def particulate_matter_10(self) -> float | None:
- """Return the particulate matter 10 level."""
- return round_state(self.coordinator.data.get(ATTR_API_PM10))
-
- @property
- def attribution(self) -> str:
- """Return the attribution."""
- return ATTRIBUTION
-
- @property
- def unique_id(self) -> str:
- """Return a unique_id for this entity."""
- return f"{self.coordinator.latitude}-{self.coordinator.longitude}"
-
- @property
- def device_info(self) -> DeviceInfo:
- """Return the device info."""
- return {
- "identifiers": {
- (
- DOMAIN,
- f"{self.coordinator.latitude}-{self.coordinator.longitude}",
- )
- },
- "name": DEFAULT_NAME,
- "manufacturer": MANUFACTURER,
- "entry_type": "service",
- }
-
- @property
- def extra_state_attributes(self) -> dict[str, Any]:
- """Return the state attributes."""
- attrs = {
- LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION],
- LABEL_ADVICE: self.coordinator.data[ATTR_API_ADVICE],
- LABEL_AQI_LEVEL: self.coordinator.data[ATTR_API_CAQI_LEVEL],
- }
- if ATTR_API_PM25 in self.coordinator.data:
- attrs[LABEL_PM_2_5_LIMIT] = self.coordinator.data[ATTR_API_PM25_LIMIT]
- attrs[LABEL_PM_2_5_PERCENT] = round(
- self.coordinator.data[ATTR_API_PM25_PERCENT]
- )
- if ATTR_API_PM10 in self.coordinator.data:
- attrs[LABEL_PM_10_LIMIT] = self.coordinator.data[ATTR_API_PM10_LIMIT]
- attrs[LABEL_PM_10_PERCENT] = round(
- self.coordinator.data[ATTR_API_PM10_PERCENT]
- )
- return attrs
-
-
-def round_state(state: float | None) -> float | None:
- """Round state."""
- return round(state) if state else state
diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py
index c3dd51e4f69..d79a33a66ab 100644
--- a/homeassistant/components/airly/const.py
+++ b/homeassistant/components/airly/const.py
@@ -3,6 +3,7 @@ from __future__ import annotations
from typing import Final
+from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
@@ -23,16 +24,22 @@ ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION"
ATTR_API_CAQI_LEVEL: Final = "LEVEL"
ATTR_API_HUMIDITY: Final = "HUMIDITY"
ATTR_API_PM10: Final = "PM10"
-ATTR_API_PM10_LIMIT: Final = "PM10_LIMIT"
-ATTR_API_PM10_PERCENT: Final = "PM10_PERCENT"
ATTR_API_PM1: Final = "PM1"
ATTR_API_PM25: Final = "PM25"
-ATTR_API_PM25_LIMIT: Final = "PM25_LIMIT"
-ATTR_API_PM25_PERCENT: Final = "PM25_PERCENT"
ATTR_API_PRESSURE: Final = "PRESSURE"
ATTR_API_TEMPERATURE: Final = "TEMPERATURE"
+
+ATTR_ADVICE: Final = "advice"
+ATTR_DESCRIPTION: Final = "description"
ATTR_LABEL: Final = "label"
+ATTR_LEVEL: Final = "level"
+ATTR_LIMIT: Final = "limit"
+ATTR_PERCENT: Final = "percent"
ATTR_UNIT: Final = "unit"
+ATTR_VALUE: Final = "value"
+
+SUFFIX_PERCENT: Final = "PERCENT"
+SUFFIX_LIMIT: Final = "LIMIT"
ATTRIBUTION: Final = "Data provided by Airly"
CONF_USE_NEAREST: Final = "use_nearest"
@@ -45,28 +52,51 @@ MIN_UPDATE_INTERVAL: Final = 5
NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet."
SENSOR_TYPES: dict[str, SensorDescription] = {
+ ATTR_API_CAQI: {
+ ATTR_LABEL: ATTR_API_CAQI,
+ ATTR_UNIT: "CAQI",
+ ATTR_VALUE: round,
+ },
ATTR_API_PM1: {
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:blur",
ATTR_LABEL: ATTR_API_PM1,
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_VALUE: round,
+ },
+ ATTR_API_PM25: {
+ ATTR_ICON: "mdi:blur",
+ ATTR_LABEL: "PM2.5",
+ ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_VALUE: round,
+ },
+ ATTR_API_PM10: {
+ ATTR_ICON: "mdi:blur",
+ ATTR_LABEL: ATTR_API_PM10,
+ ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_VALUE: round,
},
ATTR_API_HUMIDITY: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
- ATTR_ICON: None,
ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_VALUE: lambda value: round(value, 1),
},
ATTR_API_PRESSURE: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
- ATTR_ICON: None,
ATTR_LABEL: ATTR_API_PRESSURE.capitalize(),
ATTR_UNIT: PRESSURE_HPA,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_VALUE: round,
},
ATTR_API_TEMPERATURE: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
- ATTR_ICON: None,
ATTR_LABEL: ATTR_API_TEMPERATURE.capitalize(),
ATTR_UNIT: TEMP_CELSIUS,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_VALUE: lambda value: round(value, 1),
},
}
diff --git a/homeassistant/components/airly/model.py b/homeassistant/components/airly/model.py
index 42091d449e3..fe8ad6c929b 100644
--- a/homeassistant/components/airly/model.py
+++ b/homeassistant/components/airly/model.py
@@ -1,13 +1,15 @@
"""Type definitions for Airly integration."""
from __future__ import annotations
-from typing import TypedDict
+from typing import Callable, TypedDict
-class SensorDescription(TypedDict):
+class SensorDescription(TypedDict, total=False):
"""Sensor description class."""
device_class: str | None
icon: str | None
label: str
unit: str
+ state_class: str | None
+ value: Callable
diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py
index 4544a806349..3f9048dd03e 100644
--- a/homeassistant/components/airly/sensor.py
+++ b/homeassistant/components/airly/sensor.py
@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any, cast
-from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
@@ -19,15 +19,27 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirlyDataUpdateCoordinator
from .const import (
- ATTR_API_PM1,
- ATTR_API_PRESSURE,
+ ATTR_ADVICE,
+ ATTR_API_ADVICE,
+ ATTR_API_CAQI,
+ ATTR_API_CAQI_DESCRIPTION,
+ ATTR_API_CAQI_LEVEL,
+ ATTR_API_PM10,
+ ATTR_API_PM25,
+ ATTR_DESCRIPTION,
ATTR_LABEL,
+ ATTR_LEVEL,
+ ATTR_LIMIT,
+ ATTR_PERCENT,
ATTR_UNIT,
+ ATTR_VALUE,
ATTRIBUTION,
DEFAULT_NAME,
DOMAIN,
MANUFACTURER,
SENSOR_TYPES,
+ SUFFIX_LIMIT,
+ SUFFIX_PERCENT,
)
PARALLEL_UPDATES = 1
@@ -60,46 +72,49 @@ class AirlySensor(CoordinatorEntity, SensorEntity):
) -> None:
"""Initialize."""
super().__init__(coordinator)
- self._name = name
- self._description = SENSOR_TYPES[kind]
+ self._description = description = SENSOR_TYPES[kind]
+ self._attr_device_class = description.get(ATTR_DEVICE_CLASS)
+ self._attr_icon = description.get(ATTR_ICON)
+ self._attr_name = f"{name} {description[ATTR_LABEL]}"
+ self._attr_state_class = description.get(ATTR_STATE_CLASS)
+ self._attr_unique_id = (
+ f"{coordinator.latitude}-{coordinator.longitude}-{kind.lower()}"
+ )
+ self._attr_unit_of_measurement = description.get(ATTR_UNIT)
+ self._attrs: dict[str, Any] = {ATTR_ATTRIBUTION: ATTRIBUTION}
self.kind = kind
- self._state = None
- self._unit_of_measurement = None
- self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
-
- @property
- def name(self) -> str:
- """Return the name."""
- return f"{self._name} {self._description[ATTR_LABEL]}"
@property
def state(self) -> StateType:
"""Return the state."""
- self._state = self.coordinator.data[self.kind]
- if self.kind in [ATTR_API_PM1, ATTR_API_PRESSURE]:
- return round(cast(float, self._state))
- return round(cast(float, self._state), 1)
+ state = self.coordinator.data[self.kind]
+ return cast(StateType, self._description[ATTR_VALUE](state))
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
+ if self.kind == ATTR_API_CAQI:
+ self._attrs[ATTR_LEVEL] = self.coordinator.data[ATTR_API_CAQI_LEVEL]
+ self._attrs[ATTR_ADVICE] = self.coordinator.data[ATTR_API_ADVICE]
+ self._attrs[ATTR_DESCRIPTION] = self.coordinator.data[
+ ATTR_API_CAQI_DESCRIPTION
+ ]
+ if self.kind == ATTR_API_PM25:
+ self._attrs[ATTR_LIMIT] = self.coordinator.data[
+ f"{ATTR_API_PM25}_{SUFFIX_LIMIT}"
+ ]
+ self._attrs[ATTR_PERCENT] = round(
+ self.coordinator.data[f"{ATTR_API_PM25}_{SUFFIX_PERCENT}"]
+ )
+ if self.kind == ATTR_API_PM10:
+ self._attrs[ATTR_LIMIT] = self.coordinator.data[
+ f"{ATTR_API_PM10}_{SUFFIX_LIMIT}"
+ ]
+ self._attrs[ATTR_PERCENT] = round(
+ self.coordinator.data[f"{ATTR_API_PM10}_{SUFFIX_PERCENT}"]
+ )
return self._attrs
- @property
- def icon(self) -> str | None:
- """Return the icon."""
- return self._description[ATTR_ICON]
-
- @property
- def device_class(self) -> str | None:
- """Return the device_class."""
- return self._description[ATTR_DEVICE_CLASS]
-
- @property
- def unique_id(self) -> str:
- """Return a unique_id for this entity."""
- return f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}"
-
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
@@ -114,8 +129,3 @@ class AirlySensor(CoordinatorEntity, SensorEntity):
"manufacturer": MANUFACTURER,
"entry_type": "service",
}
-
- @property
- def unit_of_measurement(self) -> str | None:
- """Return the unit the value is expressed in."""
- return self._description[ATTR_UNIT]
diff --git a/homeassistant/components/airly/translations/he.json b/homeassistant/components/airly/translations/he.json
index 4c49313d977..8faa6ac2092 100644
--- a/homeassistant/components/airly/translations/he.json
+++ b/homeassistant/components/airly/translations/he.json
@@ -1,10 +1,20 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
"step": {
"user": {
"data": {
- "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
- }
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
+ "name": "\u05e9\u05dd"
+ },
+ "title": "\u05d0\u05d5\u05d5\u05e8\u05d9\u05e8\u05d9"
}
}
}
diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py
index 0b27a4a9dfd..52ee1a0e8fc 100644
--- a/homeassistant/components/airnow/__init__.py
+++ b/homeassistant/components/airnow/__init__.py
@@ -36,7 +36,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up AirNow from a config entry."""
api_key = entry.data[CONF_API_KEY]
latitude = entry.data[CONF_LATITUDE]
diff --git a/homeassistant/components/airnow/translations/de.json b/homeassistant/components/airnow/translations/de.json
index 8c2b47c1bd4..646369b6b61 100644
--- a/homeassistant/components/airnow/translations/de.json
+++ b/homeassistant/components/airnow/translations/de.json
@@ -14,7 +14,8 @@
"data": {
"api_key": "API-Schl\u00fcssel",
"latitude": "Breitengrad",
- "longitude": "L\u00e4ngengrad"
+ "longitude": "L\u00e4ngengrad",
+ "radius": "Stationsradius (Meilen; optional)"
},
"description": "Richten Sie die AirNow-Luftqualit\u00e4tsintegration ein. Um den API-Schl\u00fcssel zu generieren, besuchen Sie https://docs.airnowapi.org/account/request/.",
"title": "AirNow"
diff --git a/homeassistant/components/airnow/translations/he.json b/homeassistant/components/airnow/translations/he.json
new file mode 100644
index 00000000000..84ccc63e4db
--- /dev/null
+++ b/homeassistant/components/airnow/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "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",
+ "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": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py
index ac34c16d3d0..8a1e0ad9655 100644
--- a/homeassistant/components/airvisual/__init__.py
+++ b/homeassistant/components/airvisual/__init__.py
@@ -337,24 +337,12 @@ class AirVisualEntity(CoordinatorEntity):
"""Initialize."""
super().__init__(coordinator)
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
- self._icon = None
- self._unit = None
@property
def extra_state_attributes(self):
"""Return the device state attributes."""
return self._attrs
- @property
- def icon(self):
- """Return the icon."""
- return self._icon
-
- @property
- def unit_of_measurement(self):
- """Return the unit the value is expressed in."""
- return self._unit
-
async def async_added_to_hass(self):
"""Register callbacks."""
diff --git a/homeassistant/components/airvisual/air_quality.py b/homeassistant/components/airvisual/air_quality.py
index 047367fa67c..175c129068f 100644
--- a/homeassistant/components/airvisual/air_quality.py
+++ b/homeassistant/components/airvisual/air_quality.py
@@ -1,6 +1,5 @@
"""Support for AirVisual Node/Pro units."""
from homeassistant.components.air_quality import AirQualityEntity
-from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
from homeassistant.core import callback
from . import AirVisualEntity
@@ -34,8 +33,7 @@ class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity):
"""Initialize."""
super().__init__(airvisual)
- self._icon = "mdi:chemical-weapon"
- self._unit = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+ self._attr_icon = "mdi:chemical-weapon"
@property
def air_quality_index(self):
diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py
index 1febcec68f4..c5d6621a329 100644
--- a/homeassistant/components/airvisual/sensor.py
+++ b/homeassistant/components/airvisual/sensor.py
@@ -56,57 +56,32 @@ NODE_PRO_SENSORS = [
(SENSOR_KIND_TEMPERATURE, "Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS),
]
+POLLUTANT_LABELS = {
+ "co": "Carbon Monoxide",
+ "n2": "Nitrogen Dioxide",
+ "o3": "Ozone",
+ "p1": "PM10",
+ "p2": "PM2.5",
+ "s2": "Sulfur Dioxide",
+}
-@callback
-def async_get_pollutant_label(symbol):
- """Get a pollutant's label based on its symbol."""
- if symbol == "co":
- return "Carbon Monoxide"
- if symbol == "n2":
- return "Nitrogen Dioxide"
- if symbol == "o3":
- return "Ozone"
- if symbol == "p1":
- return "PM10"
- if symbol == "p2":
- return "PM2.5"
- if symbol == "s2":
- return "Sulfur Dioxide"
- return symbol
+POLLUTANT_LEVELS = {
+ (0, 50): ("Good", "mdi:emoticon-excited"),
+ (51, 100): ("Moderate", "mdi:emoticon-happy"),
+ (101, 150): ("Unhealthy for sensitive groups", "mdi:emoticon-neutral"),
+ (151, 200): ("Unhealthy", "mdi:emoticon-sad"),
+ (201, 300): ("Very unhealthy", "mdi:emoticon-dead"),
+ (301, 1000): ("Hazardous", "mdi:biohazard"),
+}
-
-@callback
-def async_get_pollutant_level_info(value):
- """Return a verbal pollutant level (and associated icon) for a numeric value."""
- if 0 <= value <= 50:
- return ("Good", "mdi:emoticon-excited")
- if 51 <= value <= 100:
- return ("Moderate", "mdi:emoticon-happy")
- if 101 <= value <= 150:
- return ("Unhealthy for sensitive groups", "mdi:emoticon-neutral")
- if 151 <= value <= 200:
- return ("Unhealthy", "mdi:emoticon-sad")
- if 201 <= value <= 300:
- return ("Very Unhealthy", "mdi:emoticon-dead")
- return ("Hazardous", "mdi:biohazard")
-
-
-@callback
-def async_get_pollutant_unit(symbol):
- """Get a pollutant's unit based on its symbol."""
- if symbol == "co":
- return CONCENTRATION_PARTS_PER_MILLION
- if symbol == "n2":
- return CONCENTRATION_PARTS_PER_BILLION
- if symbol == "o3":
- return CONCENTRATION_PARTS_PER_BILLION
- if symbol == "p1":
- return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
- if symbol == "p2":
- return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
- if symbol == "s2":
- return CONCENTRATION_PARTS_PER_BILLION
- return None
+POLLUTANT_UNITS = {
+ "co": CONCENTRATION_PARTS_PER_MILLION,
+ "n2": CONCENTRATION_PARTS_PER_BILLION,
+ "o3": CONCENTRATION_PARTS_PER_BILLION,
+ "p1": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ "p2": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ "s2": CONCENTRATION_PARTS_PER_BILLION,
+}
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -154,12 +129,13 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
}
)
self._config_entry = config_entry
- self._icon = icon
self._kind = kind
self._locale = locale
self._name = name
self._state = None
- self._unit = unit
+
+ self._attr_icon = icon
+ self._attr_unit_of_measurement = unit
@property
def available(self):
@@ -196,16 +172,20 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
if self._kind == SENSOR_KIND_LEVEL:
aqi = data[f"aqi{self._locale}"]
- self._state, self._icon = async_get_pollutant_level_info(aqi)
+ [(self._state, self._attr_icon)] = [
+ (name, icon)
+ for (floor, ceiling), (name, icon) in POLLUTANT_LEVELS.items()
+ if floor <= aqi <= ceiling
+ ]
elif self._kind == SENSOR_KIND_AQI:
self._state = data[f"aqi{self._locale}"]
elif self._kind == SENSOR_KIND_POLLUTANT:
symbol = data[f"main{self._locale}"]
- self._state = async_get_pollutant_label(symbol)
+ self._state = POLLUTANT_LABELS[symbol]
self._attrs.update(
{
ATTR_POLLUTANT_SYMBOL: symbol,
- ATTR_POLLUTANT_UNIT: async_get_pollutant_unit(symbol),
+ ATTR_POLLUTANT_UNIT: POLLUTANT_UNITS[symbol],
}
)
@@ -244,16 +224,12 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity):
"""Initialize."""
super().__init__(coordinator)
- self._device_class = device_class
self._kind = kind
self._name = name
self._state = None
- self._unit = unit
- @property
- def device_class(self):
- """Return the device class."""
- return self._device_class
+ self._attr_device_class = device_class
+ self._attr_unit_of_measurement = unit
@property
def device_info(self):
diff --git a/homeassistant/components/airvisual/translations/he.json b/homeassistant/components/airvisual/translations/he.json
index 7fc0c2983df..5dfc5cbdd73 100644
--- a/homeassistant/components/airvisual/translations/he.json
+++ b/homeassistant/components/airvisual/translations/he.json
@@ -1,12 +1,37 @@
{
"config": {
+ "abort": {
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
"error": {
- "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05e1\u05d5\u05e4\u05e7"
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "general_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4",
+ "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05e1\u05d5\u05e4\u05e7",
+ "location_not_found": "\u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0"
},
"step": {
+ "geography_by_coords": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API"
+ }
+ },
"node_pro": {
"data": {
+ "ip_address": "\u05de\u05d0\u05e8\u05d7",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "description": "\u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d9\u05d7\u05d9\u05d3\u05ea AirVisual \u05d0\u05d9\u05e9\u05d9\u05ea. \u05e0\u05d9\u05ea\u05df \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05de\u05de\u05e9\u05e7 \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05dc \u05d4\u05d9\u05d7\u05d9\u05d3\u05d4."
+ },
+ "reauth_confirm": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API"
}
}
}
diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py
index 2d6d1f4d5b1..c8da648fec6 100644
--- a/homeassistant/components/alarm_control_panel/__init__.py
+++ b/homeassistant/components/alarm_control_panel/__init__.py
@@ -1,7 +1,6 @@
"""Component to interface with an alarm control panel."""
from __future__ import annotations
-from abc import abstractmethod
from datetime import timedelta
import logging
from typing import Any, Final, final
@@ -113,20 +112,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
class AlarmControlPanelEntity(Entity):
"""An abstract class for alarm control entities."""
+ _attr_changed_by: str | None = None
+ _attr_code_arm_required: bool = True
+ _attr_code_format: str | None = None
+ _attr_supported_features: int
+
@property
def code_format(self) -> str | None:
"""Regex for code format or None if no code is required."""
- return None
+ return self._attr_code_format
@property
def changed_by(self) -> str | None:
"""Last change triggered by."""
- return None
+ return self._attr_changed_by
@property
def code_arm_required(self) -> bool:
"""Whether the code is required for arm actions."""
- return True
+ return self._attr_code_arm_required
def alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
@@ -177,9 +181,9 @@ class AlarmControlPanelEntity(Entity):
await self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code)
@property
- @abstractmethod
def supported_features(self) -> int:
"""Return the list of supported features."""
+ return self._attr_supported_features
@final
@property
diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py
index 506552f8a50..d92f9615c9a 100644
--- a/homeassistant/components/alarm_control_panel/device_action.py
+++ b/homeassistant/components/alarm_control_panel/device_action.py
@@ -8,7 +8,6 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_CODE,
ATTR_ENTITY_ID,
- ATTR_SUPPORTED_FEATURES,
CONF_CODE,
CONF_DEVICE_ID,
CONF_DOMAIN,
@@ -23,6 +22,7 @@ from homeassistant.const import (
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.typing import ConfigType
from . import ATTR_CODE_ARM_REQUIRED, DOMAIN
@@ -62,59 +62,24 @@ async def async_get_actions(
if entry.domain != DOMAIN:
continue
- state = hass.states.get(entry.entity_id)
+ supported_features = get_supported_features(hass, entry.entity_id)
- # We need a state or else we can't populate the HVAC and preset modes.
- if state is None:
- continue
-
- supported_features = state.attributes[ATTR_SUPPORTED_FEATURES]
+ base_action = {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
# Add actions for each entity that belongs to this integration
if supported_features & SUPPORT_ALARM_ARM_AWAY:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "arm_away",
- }
- )
+ actions.append({**base_action, CONF_TYPE: "arm_away"})
if supported_features & SUPPORT_ALARM_ARM_HOME:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "arm_home",
- }
- )
+ actions.append({**base_action, CONF_TYPE: "arm_home"})
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "arm_night",
- }
- )
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "disarm",
- }
- )
+ actions.append({**base_action, CONF_TYPE: "arm_night"})
+ actions.append({**base_action, CONF_TYPE: "disarm"})
if supported_features & SUPPORT_ALARM_TRIGGER:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "trigger",
- }
- )
+ actions.append({**base_action, CONF_TYPE: "trigger"})
return actions
@@ -147,6 +112,8 @@ async def async_get_action_capabilities(
hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:
"""List action capabilities."""
+ # We need to refer to the state directly because ATTR_CODE_ARM_REQUIRED is not a
+ # capability attribute
state = hass.states.get(config[CONF_ENTITY_ID])
code_required = state.attributes.get(ATTR_CODE_ARM_REQUIRED) if state else False
diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py
index fa4f903f2e5..3cbaa019ad0 100644
--- a/homeassistant/components/alarm_control_panel/device_condition.py
+++ b/homeassistant/components/alarm_control_panel/device_condition.py
@@ -13,7 +13,6 @@ from homeassistant.components.alarm_control_panel.const import (
)
from homeassistant.const import (
ATTR_ENTITY_ID,
- ATTR_SUPPORTED_FEATURES,
CONF_CONDITION,
CONF_DEVICE_ID,
CONF_DOMAIN,
@@ -29,6 +28,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers import condition, config_validation as cv, entity_registry
from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
+from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN
@@ -70,70 +70,29 @@ async def async_get_conditions(
if entry.domain != DOMAIN:
continue
- state = hass.states.get(entry.entity_id)
-
- # We need a state or else we can't populate the different armed conditions
- if state is None:
- continue
-
- supported_features = state.attributes[ATTR_SUPPORTED_FEATURES]
+ supported_features = get_supported_features(hass, entry.entity_id)
# Add conditions for each entity that belongs to this integration
+ base_condition = {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
conditions += [
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: CONDITION_DISARMED,
- },
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: CONDITION_TRIGGERED,
- },
+ {**base_condition, CONF_TYPE: CONDITION_DISARMED},
+ {**base_condition, CONF_TYPE: CONDITION_TRIGGERED},
]
if supported_features & SUPPORT_ALARM_ARM_HOME:
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: CONDITION_ARMED_HOME,
- }
- )
+ conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_HOME})
if supported_features & SUPPORT_ALARM_ARM_AWAY:
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: CONDITION_ARMED_AWAY,
- }
- )
+ conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_AWAY})
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: CONDITION_ARMED_NIGHT,
- }
- )
+ conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_NIGHT})
if supported_features & SUPPORT_ALARM_ARM_CUSTOM_BYPASS:
conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: CONDITION_ARMED_CUSTOM_BYPASS,
- }
+ {**base_condition, CONF_TYPE: CONDITION_ARMED_CUSTOM_BYPASS}
)
return conditions
diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py
index 477a0c0fe6d..f89e03e7326 100644
--- a/homeassistant/components/alarm_control_panel/device_trigger.py
+++ b/homeassistant/components/alarm_control_panel/device_trigger.py
@@ -11,10 +11,9 @@ from homeassistant.components.alarm_control_panel.const import (
SUPPORT_ALARM_ARM_NIGHT,
)
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import state as state_trigger
from homeassistant.const import (
- ATTR_SUPPORTED_FEATURES,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
@@ -30,6 +29,7 @@ from homeassistant.const import (
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_registry
+from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.typing import ConfigType
from . import DOMAIN
@@ -41,7 +41,7 @@ TRIGGER_TYPES: Final[set[str]] = BASIC_TRIGGER_TYPES | {
"armed_night",
}
-TRIGGER_SCHEMA: Final = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
@@ -62,13 +62,7 @@ async def async_get_triggers(
if entry.domain != DOMAIN:
continue
- entity_state = hass.states.get(entry.entity_id)
-
- # We need a state or else we can't populate the HVAC and preset modes.
- if entity_state is None:
- continue
-
- supported_features = entity_state.attributes[ATTR_SUPPORTED_FEATURES]
+ supported_features = get_supported_features(hass, entry.entity_id)
# Add triggers for each entity that belongs to this integration
base_trigger = {
diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json
index aea85f49a59..4936cce4dfd 100644
--- a/homeassistant/components/alarmdecoder/translations/de.json
+++ b/homeassistant/components/alarmdecoder/translations/de.json
@@ -16,40 +16,58 @@
"device_path": "Ger\u00e4tepfad",
"host": "Host",
"port": "Port"
- }
+ },
+ "title": "Verbindungseinstellungen konfigurieren"
},
"user": {
"data": {
"protocol": "Protokoll"
- }
+ },
+ "title": "W\u00e4hlen Sie das AlarmDecoder-Protokoll"
}
}
},
"options": {
+ "error": {
+ "int": "Das Feld unten muss eine ganze Zahl sein.",
+ "loop_range": "RF Loop muss eine ganze Zahl zwischen 1 und 4 sein.",
+ "loop_rfid": "RF Loop kann nicht ohne RF Serial verwendet werden.",
+ "relay_inclusive": "Relaisadresse und Relaiskanal sind abh\u00e4ngig voneinander und m\u00fcssen zusammen aufgenommen werden."
+ },
"step": {
"arm_settings": {
"data": {
- "alt_night_mode": "Alternativer Nachtmodus"
- }
+ "alt_night_mode": "Alternativer Nachtmodus",
+ "auto_bypass": "Automatischer Bypass bei Scharfschaltung",
+ "code_arm_required": "Code f\u00fcr Scharfschaltung erforderlich"
+ },
+ "title": "AlarmDecoder konfigurieren"
},
"init": {
"data": {
"edit_select": "Bearbeiten"
},
- "description": "Was m\u00f6chtest du bearbeiten?"
+ "description": "Was m\u00f6chtest du bearbeiten?",
+ "title": "AlarmDecoder konfigurieren"
},
"zone_details": {
"data": {
+ "zone_loop": "RF Loop",
"zone_name": "Zonenname",
"zone_relayaddr": "Relais-Adresse",
+ "zone_relaychan": "Relaiskanal",
+ "zone_rfid": "RF Serial",
"zone_type": "Zonentyp"
- }
+ },
+ "description": "Geben Sie Details f\u00fcr Zone {zone_number} ein. Um Zone {zone_number} zu l\u00f6schen, lassen Sie Zonenname leer.",
+ "title": "AlarmDecoder konfigurieren"
},
"zone_select": {
"data": {
"zone_number": "Zonennummer"
},
- "description": "Gib die die Zonennummer ein, die du hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chtest."
+ "description": "Gib die die Zonennummer ein, die du hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chtest.",
+ "title": "AlarmDecoder konfigurieren"
}
}
}
diff --git a/homeassistant/components/alarmdecoder/translations/he.json b/homeassistant/components/alarmdecoder/translations/he.json
new file mode 100644
index 00000000000..e130a1997b2
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/he.json
@@ -0,0 +1,41 @@
+{
+ "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": {
+ "protocol": {
+ "data": {
+ "device_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df",
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ },
+ "user": {
+ "data": {
+ "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "relay_inclusive": "\u05db\u05ea\u05d5\u05d1\u05ea \u05de\u05de\u05e1\u05e8 \u05d5\u05e2\u05e8\u05d5\u05e5 \u05de\u05de\u05e1\u05e8 \u05d4\u05dd \u05ea\u05dc\u05d5\u05d9\u05d9 \u05e7\u05d5\u05d3 \u05d5\u05d9\u05e9 \u05dc\u05db\u05dc\u05d5\u05dc \u05d0\u05d5\u05ea\u05dd \u05d9\u05d7\u05d3."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "edit_select": "\u05e2\u05e8\u05d5\u05da"
+ }
+ },
+ "zone_details": {
+ "data": {
+ "zone_relaychan": "\u05e2\u05e8\u05d5\u05e5 \u05de\u05de\u05e1\u05e8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py
index 483b4484261..10b382c8dcf 100644
--- a/homeassistant/components/alexa/capabilities.py
+++ b/homeassistant/components/alexa/capabilities.py
@@ -1155,8 +1155,6 @@ class AlexaPowerLevelController(AlexaCapability):
if self.entity.domain == fan.DOMAIN:
return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
- return None
-
class AlexaSecurityPanelController(AlexaCapability):
"""Implements Alexa.SecurityPanelController.
@@ -1304,6 +1302,12 @@ class AlexaModeController(AlexaCapability):
if mode in (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE, STATE_UNKNOWN):
return f"{fan.ATTR_DIRECTION}.{mode}"
+ # Fan preset_mode
+ if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
+ mode = self.entity.attributes.get(fan.ATTR_PRESET_MODE, None)
+ if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, None):
+ return f"{fan.ATTR_PRESET_MODE}.{mode}"
+
# Cover Position
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
# Return state instead of position when using ModeController.
@@ -1342,6 +1346,17 @@ class AlexaModeController(AlexaCapability):
)
return self._resource.serialize_capability_resources()
+ # Fan preset_mode
+ if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
+ self._resource = AlexaModeResource(
+ [AlexaGlobalCatalog.SETTING_PRESET], False
+ )
+ for preset_mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, []):
+ self._resource.add_mode(
+ f"{fan.ATTR_PRESET_MODE}.{preset_mode}", [preset_mode]
+ )
+ return self._resource.serialize_capability_resources()
+
# Cover Position Resources
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
self._resource = AlexaModeResource(
diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py
index 723d115b923..cef18623bf5 100644
--- a/homeassistant/components/alexa/entities.py
+++ b/homeassistant/components/alexa/entities.py
@@ -535,6 +535,7 @@ class FanCapabilities(AlexaEntity):
if supported & fan.SUPPORT_SET_SPEED:
yield AlexaPercentageController(self.entity)
yield AlexaPowerLevelController(self.entity)
+ # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
yield AlexaRangeController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}"
)
@@ -542,6 +543,10 @@ class FanCapabilities(AlexaEntity):
yield AlexaToggleController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
)
+ if supported & fan.SUPPORT_PRESET_MODE:
+ yield AlexaModeController(
+ self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
+ )
if supported & fan.SUPPORT_DIRECTION:
yield AlexaModeController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}"
diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py
index da0011f817a..01d1369eb2f 100644
--- a/homeassistant/components/alexa/handlers.py
+++ b/homeassistant/components/alexa/handlers.py
@@ -62,7 +62,6 @@ from .errors import (
AlexaInvalidDirectiveError,
AlexaInvalidValueError,
AlexaSecurityPanelAuthorizationRequired,
- AlexaSecurityPanelUnauthorizedError,
AlexaTempRangeError,
AlexaUnsupportedThermostatModeError,
AlexaVideoActionNotPermittedForContentError,
@@ -927,11 +926,9 @@ async def async_api_disarm(hass, config, directive, context):
if payload["authorization"]["type"] == "FOUR_DIGIT_PIN":
data["code"] = value
- if not await hass.services.async_call(
+ await hass.services.async_call(
entity.domain, SERVICE_ALARM_DISARM, data, blocking=True, context=context
- ):
- msg = "Invalid Code"
- raise AlexaSecurityPanelUnauthorizedError(msg)
+ )
response.add_context_property(
{
@@ -961,6 +958,16 @@ async def async_api_set_mode(hass, config, directive, context):
service = fan.SERVICE_SET_DIRECTION
data[fan.ATTR_DIRECTION] = direction
+ # Fan preset_mode
+ elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
+ preset_mode = mode.split(".")[1]
+ if preset_mode in entity.attributes.get(fan.ATTR_PRESET_MODES):
+ service = fan.SERVICE_SET_PRESET_MODE
+ data[fan.ATTR_PRESET_MODE] = preset_mode
+ else:
+ msg = f"Entity '{entity.entity_id}' does not support Preset '{preset_mode}'"
+ raise AlexaInvalidValueError(msg)
+
# Cover Position
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
position = mode.split(".")[1]
diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py
index 0c012788b0e..5d3b5a86942 100644
--- a/homeassistant/components/almond/__init__.py
+++ b/homeassistant/components/almond/__init__.py
@@ -11,9 +11,9 @@ import async_timeout
from pyalmond import AbstractAlmondWebAuth, AlmondLocalAuth, WebAlmondAPI
import voluptuous as vol
-from homeassistant import config_entries
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.components import conversation
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
@@ -94,14 +94,14 @@ async def async_setup(hass, config):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
- context={"source": config_entries.SOURCE_IMPORT},
+ context={"source": SOURCE_IMPORT},
data={"type": TYPE_LOCAL, "host": conf[CONF_HOST]},
)
)
return True
-async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Almond config entry."""
websession = aiohttp_client.async_get_clientsession(hass)
@@ -150,7 +150,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEnt
async def _configure_almond_for_ha(
- hass: HomeAssistant, entry: config_entries.ConfigEntry, api: WebAlmondAPI
+ hass: HomeAssistant, entry: ConfigEntry, api: WebAlmondAPI
):
"""Configure Almond to connect to HA."""
try:
@@ -248,7 +248,7 @@ class AlmondAgent(conversation.AbstractConversationAgent):
"""Almond conversation agent."""
def __init__(
- self, hass: HomeAssistant, api: WebAlmondAPI, entry: config_entries.ConfigEntry
+ self, hass: HomeAssistant, api: WebAlmondAPI, entry: ConfigEntry
) -> None:
"""Initialize the agent."""
self.hass = hass
diff --git a/homeassistant/components/almond/translations/he.json b/homeassistant/components/almond/translations/he.json
new file mode 100644
index 00000000000..6aa9dd1d75f
--- /dev/null
+++ b/homeassistant/components/almond/translations/he.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+ "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})",
+ "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": {
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py
new file mode 100644
index 00000000000..362dc26d851
--- /dev/null
+++ b/homeassistant/components/ambee/__init__.py
@@ -0,0 +1,71 @@
+"""Support for Ambee."""
+from __future__ import annotations
+
+from ambee import AirQuality, Ambee, AmbeeAuthenticationError, Pollen
+
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryAuthFailed
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_POLLEN
+
+PLATFORMS = (SENSOR_DOMAIN,)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up Ambee from a config entry."""
+ hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})
+
+ client = Ambee(
+ api_key=entry.data[CONF_API_KEY],
+ latitude=entry.data[CONF_LATITUDE],
+ longitude=entry.data[CONF_LONGITUDE],
+ )
+
+ async def update_air_quality() -> AirQuality:
+ """Update method for updating Ambee Air Quality data."""
+ try:
+ return await client.air_quality()
+ except AmbeeAuthenticationError as err:
+ raise ConfigEntryAuthFailed from err
+
+ air_quality: DataUpdateCoordinator[AirQuality] = DataUpdateCoordinator(
+ hass,
+ LOGGER,
+ name=f"{DOMAIN}_{SERVICE_AIR_QUALITY}",
+ update_interval=SCAN_INTERVAL,
+ update_method=update_air_quality,
+ )
+ await air_quality.async_config_entry_first_refresh()
+ hass.data[DOMAIN][entry.entry_id][SERVICE_AIR_QUALITY] = air_quality
+
+ async def update_pollen() -> Pollen:
+ """Update method for updating Ambee Pollen data."""
+ try:
+ return await client.pollen()
+ except AmbeeAuthenticationError as err:
+ raise ConfigEntryAuthFailed from err
+
+ pollen: DataUpdateCoordinator[Pollen] = DataUpdateCoordinator(
+ hass,
+ LOGGER,
+ name=f"{DOMAIN}_{SERVICE_POLLEN}",
+ update_interval=SCAN_INTERVAL,
+ update_method=update_pollen,
+ )
+ await pollen.async_config_entry_first_refresh()
+ hass.data[DOMAIN][entry.entry_id][SERVICE_POLLEN] = pollen
+
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload Ambee config entry."""
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ if unload_ok:
+ del hass.data[DOMAIN][entry.entry_id]
+ return unload_ok
diff --git a/homeassistant/components/ambee/config_flow.py b/homeassistant/components/ambee/config_flow.py
new file mode 100644
index 00000000000..0550c541ed0
--- /dev/null
+++ b/homeassistant/components/ambee/config_flow.py
@@ -0,0 +1,115 @@
+"""Config flow to configure the Ambee integration."""
+from __future__ import annotations
+
+from typing import Any
+
+from ambee import Ambee, AmbeeAuthenticationError, AmbeeError
+import voluptuous as vol
+
+from homeassistant.config_entries import ConfigEntry, ConfigFlow
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+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 DOMAIN
+
+
+class AmbeeFlowHandler(ConfigFlow, domain=DOMAIN):
+ """Config flow for Ambee."""
+
+ VERSION = 1
+
+ entry: ConfigEntry | None = None
+
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Handle a flow initialized by the user."""
+ errors = {}
+
+ if user_input is not None:
+ session = async_get_clientsession(self.hass)
+ try:
+ client = Ambee(
+ api_key=user_input[CONF_API_KEY],
+ latitude=user_input[CONF_LATITUDE],
+ longitude=user_input[CONF_LONGITUDE],
+ session=session,
+ )
+ await client.air_quality()
+ except AmbeeAuthenticationError:
+ errors["base"] = "invalid_api_key"
+ except AmbeeError:
+ errors["base"] = "cannot_connect"
+ else:
+ return self.async_create_entry(
+ title=user_input[CONF_NAME],
+ data={
+ CONF_API_KEY: user_input[CONF_API_KEY],
+ CONF_LATITUDE: user_input[CONF_LATITUDE],
+ CONF_LONGITUDE: user_input[CONF_LONGITUDE],
+ },
+ )
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_API_KEY): str,
+ vol.Optional(
+ CONF_NAME, default=self.hass.config.location_name
+ ): str,
+ vol.Optional(
+ CONF_LATITUDE, default=self.hass.config.latitude
+ ): cv.latitude,
+ vol.Optional(
+ CONF_LONGITUDE, default=self.hass.config.longitude
+ ): cv.longitude,
+ }
+ ),
+ errors=errors,
+ )
+
+ async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult:
+ """Handle initiation of re-authentication with Ambee."""
+ 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:
+ """Handle re-authentication with Ambee."""
+ errors = {}
+ if user_input is not None and self.entry:
+ session = async_get_clientsession(self.hass)
+ client = Ambee(
+ api_key=user_input[CONF_API_KEY],
+ latitude=self.entry.data[CONF_LATITUDE],
+ longitude=self.entry.data[CONF_LONGITUDE],
+ session=session,
+ )
+ try:
+ await client.air_quality()
+ except AmbeeAuthenticationError:
+ errors["base"] = "invalid_api_key"
+ except AmbeeError:
+ errors["base"] = "cannot_connect"
+ else:
+ self.hass.config_entries.async_update_entry(
+ self.entry,
+ data={
+ **self.entry.data,
+ CONF_API_KEY: user_input[CONF_API_KEY],
+ },
+ )
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
+
+ return self.async_show_form(
+ step_id="reauth_confirm",
+ data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
+ errors=errors,
+ )
diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py
new file mode 100644
index 00000000000..730c6780f4f
--- /dev/null
+++ b/homeassistant/components/ambee/const.py
@@ -0,0 +1,212 @@
+"""Constants for the Ambee integration."""
+from __future__ import annotations
+
+from datetime import timedelta
+import logging
+from typing import Final
+
+from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_ICON,
+ ATTR_NAME,
+ ATTR_UNIT_OF_MEASUREMENT,
+ CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ CONCENTRATION_PARTS_PER_BILLION,
+ CONCENTRATION_PARTS_PER_CUBIC_METER,
+ CONCENTRATION_PARTS_PER_MILLION,
+ DEVICE_CLASS_CO,
+)
+
+from .models import AmbeeSensor
+
+DOMAIN: Final = "ambee"
+LOGGER = logging.getLogger(__package__)
+SCAN_INTERVAL = timedelta(hours=1)
+
+ATTR_ENABLED_BY_DEFAULT: Final = "enabled_by_default"
+ATTR_ENTRY_TYPE: Final = "entry_type"
+ENTRY_TYPE_SERVICE: Final = "service"
+
+DEVICE_CLASS_AMBEE_RISK: Final = "ambee__risk"
+
+SERVICE_AIR_QUALITY: Final = "air_quality"
+SERVICE_POLLEN: Final = "pollen"
+
+SERVICES: dict[str, str] = {
+ SERVICE_AIR_QUALITY: "Air Quality",
+ SERVICE_POLLEN: "Pollen",
+}
+
+SENSORS: dict[str, dict[str, AmbeeSensor]] = {
+ SERVICE_AIR_QUALITY: {
+ "particulate_matter_2_5": {
+ ATTR_NAME: "Particulate Matter < 2.5 μm",
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ },
+ "particulate_matter_10": {
+ ATTR_NAME: "Particulate Matter < 10 μm",
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ },
+ "sulphur_dioxide": {
+ ATTR_NAME: "Sulphur Dioxide (SO2)",
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ },
+ "nitrogen_dioxide": {
+ ATTR_NAME: "Nitrogen Dioxide (NO2)",
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ },
+ "ozone": {
+ ATTR_NAME: "Ozone",
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ },
+ "carbon_monoxide": {
+ ATTR_NAME: "Carbon Monoxide (CO)",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_CO,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ },
+ "air_quality_index": {
+ ATTR_NAME: "Air Quality Index (AQI)",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ },
+ },
+ SERVICE_POLLEN: {
+ "grass": {
+ ATTR_NAME: "Grass Pollen",
+ ATTR_ICON: "mdi:grass",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ },
+ "tree": {
+ ATTR_NAME: "Tree Pollen",
+ ATTR_ICON: "mdi:tree",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ },
+ "weed": {
+ ATTR_NAME: "Weed Pollen",
+ ATTR_ICON: "mdi:sprout",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ },
+ "grass_risk": {
+ ATTR_NAME: "Grass Pollen Risk",
+ ATTR_ICON: "mdi:grass",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK,
+ },
+ "tree_risk": {
+ ATTR_NAME: "Tree Pollen Risk",
+ ATTR_ICON: "mdi:tree",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK,
+ },
+ "weed_risk": {
+ ATTR_NAME: "Weed Pollen Risk",
+ ATTR_ICON: "mdi:sprout",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK,
+ },
+ "grass_poaceae": {
+ ATTR_NAME: "Poaceae Grass Pollen",
+ ATTR_ICON: "mdi:grass",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "tree_alder": {
+ ATTR_NAME: "Alder Tree Pollen",
+ ATTR_ICON: "mdi:tree",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "tree_birch": {
+ ATTR_NAME: "Birch Tree Pollen",
+ ATTR_ICON: "mdi:tree",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "tree_cypress": {
+ ATTR_NAME: "Cypress Tree Pollen",
+ ATTR_ICON: "mdi:tree",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "tree_elm": {
+ ATTR_NAME: "Elm Tree Pollen",
+ ATTR_ICON: "mdi:tree",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "tree_hazel": {
+ ATTR_NAME: "Hazel Tree Pollen",
+ ATTR_ICON: "mdi:tree",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "tree_oak": {
+ ATTR_NAME: "Oak Tree Pollen",
+ ATTR_ICON: "mdi:tree",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "tree_pine": {
+ ATTR_NAME: "Pine Tree Pollen",
+ ATTR_ICON: "mdi:tree",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "tree_plane": {
+ ATTR_NAME: "Plane Tree Pollen",
+ ATTR_ICON: "mdi:tree",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "tree_poplar": {
+ ATTR_NAME: "Poplar Tree Pollen",
+ ATTR_ICON: "mdi:tree",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "weed_chenopod": {
+ ATTR_NAME: "Chenopod Weed Pollen",
+ ATTR_ICON: "mdi:sprout",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "weed_mugwort": {
+ ATTR_NAME: "Mugwort Weed Pollen",
+ ATTR_ICON: "mdi:sprout",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "weed_nettle": {
+ ATTR_NAME: "Nettle Weed Pollen",
+ ATTR_ICON: "mdi:sprout",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ "weed_ragweed": {
+ ATTR_NAME: "Ragweed Weed Pollen",
+ ATTR_ICON: "mdi:sprout",
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
+ ATTR_ENABLED_BY_DEFAULT: False,
+ },
+ },
+}
diff --git a/homeassistant/components/ambee/manifest.json b/homeassistant/components/ambee/manifest.json
new file mode 100644
index 00000000000..e546f5009e8
--- /dev/null
+++ b/homeassistant/components/ambee/manifest.json
@@ -0,0 +1,10 @@
+{
+ "domain": "ambee",
+ "name": "Ambee",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/ambee",
+ "requirements": ["ambee==0.3.0"],
+ "codeowners": ["@frenck"],
+ "quality_scale": "platinum",
+ "iot_class": "cloud_polling"
+}
diff --git a/homeassistant/components/ambee/models.py b/homeassistant/components/ambee/models.py
new file mode 100644
index 00000000000..871aeed332b
--- /dev/null
+++ b/homeassistant/components/ambee/models.py
@@ -0,0 +1,15 @@
+"""Models helper class for the Ambee integration."""
+from __future__ import annotations
+
+from typing import TypedDict
+
+
+class AmbeeSensor(TypedDict, total=False):
+ """Represent an Ambee Sensor."""
+
+ device_class: str
+ enabled_by_default: bool
+ icon: str
+ name: str
+ state_class: str
+ unit_of_measurement: str
diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py
new file mode 100644
index 00000000000..54e67160822
--- /dev/null
+++ b/homeassistant/components/ambee/sensor.py
@@ -0,0 +1,99 @@
+"""Support for Ambee sensors."""
+from __future__ import annotations
+
+from homeassistant.components.sensor import (
+ ATTR_STATE_CLASS,
+ DOMAIN as SENSOR_DOMAIN,
+ SensorEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_ICON,
+ ATTR_IDENTIFIERS,
+ ATTR_MANUFACTURER,
+ ATTR_NAME,
+ ATTR_UNIT_OF_MEASUREMENT,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import StateType
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
+
+from .const import (
+ ATTR_ENABLED_BY_DEFAULT,
+ ATTR_ENTRY_TYPE,
+ DOMAIN,
+ ENTRY_TYPE_SERVICE,
+ SENSORS,
+ SERVICES,
+)
+from .models import AmbeeSensor
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up Ambee sensors based on a config entry."""
+ async_add_entities(
+ AmbeeSensorEntity(
+ coordinator=hass.data[DOMAIN][entry.entry_id][service_key],
+ entry_id=entry.entry_id,
+ sensor_key=sensor_key,
+ sensor=sensor,
+ service_key=service_key,
+ service=SERVICES[service_key],
+ )
+ for service_key, service_sensors in SENSORS.items()
+ for sensor_key, sensor in service_sensors.items()
+ )
+
+
+class AmbeeSensorEntity(CoordinatorEntity, SensorEntity):
+ """Defines an Ambee sensor."""
+
+ def __init__(
+ self,
+ *,
+ coordinator: DataUpdateCoordinator,
+ entry_id: str,
+ sensor_key: str,
+ sensor: AmbeeSensor,
+ service_key: str,
+ service: str,
+ ) -> None:
+ """Initialize Ambee sensor."""
+ super().__init__(coordinator=coordinator)
+ self._sensor_key = sensor_key
+ self._service_key = service_key
+
+ self.entity_id = f"{SENSOR_DOMAIN}.{service_key}_{sensor_key}"
+ self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS)
+ self._attr_entity_registry_enabled_default = sensor.get(
+ ATTR_ENABLED_BY_DEFAULT, True
+ )
+ self._attr_icon = sensor.get(ATTR_ICON)
+ self._attr_name = sensor.get(ATTR_NAME)
+ self._attr_state_class = sensor.get(ATTR_STATE_CLASS)
+ self._attr_unique_id = f"{entry_id}_{service_key}_{sensor_key}"
+ self._attr_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT)
+
+ self._attr_device_info = {
+ ATTR_IDENTIFIERS: {(DOMAIN, f"{entry_id}_{service_key}")},
+ ATTR_NAME: service,
+ ATTR_MANUFACTURER: "Ambee",
+ ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE,
+ }
+
+ @property
+ def state(self) -> StateType:
+ """Return the state of the sensor."""
+ value = getattr(self.coordinator.data, self._sensor_key)
+ if isinstance(value, str):
+ return value.lower()
+ return value # type: ignore[no-any-return]
diff --git a/homeassistant/components/ambee/strings.json b/homeassistant/components/ambee/strings.json
new file mode 100644
index 00000000000..e3c306788dd
--- /dev/null
+++ b/homeassistant/components/ambee/strings.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "description": "Set up Ambee to integrate with Home Assistant.",
+ "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": "[%key:common::config_flow::data::name%]"
+ }
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Re-authenticate with your Ambee account.",
+ "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%]"
+ },
+ "abort": {
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ }
+ }
+}
diff --git a/homeassistant/components/ambee/strings.sensor.json b/homeassistant/components/ambee/strings.sensor.json
new file mode 100644
index 00000000000..83eb3b3fd73
--- /dev/null
+++ b/homeassistant/components/ambee/strings.sensor.json
@@ -0,0 +1,10 @@
+{
+ "state": {
+ "ambee__risk": {
+ "low": "Low",
+ "moderate": "Moderate",
+ "high": "High",
+ "very high": "Very High"
+ }
+ }
+}
diff --git a/homeassistant/components/ambee/translations/ca.json b/homeassistant/components/ambee/translations/ca.json
new file mode 100644
index 00000000000..ab3c9cb949e
--- /dev/null
+++ b/homeassistant/components/ambee/translations/ca.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_api_key": "Clau API inv\u00e0lida"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "api_key": "Clau API",
+ "description": "Torna a autenticar-te amb el compte d'Ambee."
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "Clau API",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "name": "Nom"
+ },
+ "description": "Configura Ambee per a integrar-lo amb Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/de.json b/homeassistant/components/ambee/translations/de.json
new file mode 100644
index 00000000000..4359ab72349
--- /dev/null
+++ b/homeassistant/components/ambee/translations/de.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "api_key": "API-Schl\u00fcssel",
+ "description": "Authentifiziere dich erneut mit deinem Ambee-Konto."
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "API-Schl\u00fcssel",
+ "latitude": "Breitengrad",
+ "longitude": "L\u00e4ngengrad",
+ "name": "Name"
+ },
+ "description": "Richte Ambee f\u00fcr die Integration mit Home Assistant ein."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/en.json b/homeassistant/components/ambee/translations/en.json
new file mode 100644
index 00000000000..433580e8023
--- /dev/null
+++ b/homeassistant/components/ambee/translations/en.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Re-authentication was successful"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_api_key": "Invalid API key"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "api_key": "API Key",
+ "description": "Re-authenticate with your Ambee account."
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "API Key",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Name"
+ },
+ "description": "Set up Ambee to integrate with Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/es.json b/homeassistant/components/ambee/translations/es.json
new file mode 100644
index 00000000000..de5ce971fa0
--- /dev/null
+++ b/homeassistant/components/ambee/translations/es.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "description": "Vuelva a autenticarse con su cuenta de Ambee."
+ }
+ },
+ "user": {
+ "description": "Configure Ambee para que se integre con Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/et.json b/homeassistant/components/ambee/translations/et.json
new file mode 100644
index 00000000000..085f13d6926
--- /dev/null
+++ b/homeassistant/components/ambee/translations/et.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendumine nurjus",
+ "invalid_api_key": "Vale API v\u00f5ti"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "api_key": "API v\u00f5ti",
+ "description": "Taastuvasta Ambee konto"
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "API v\u00f5ti",
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad",
+ "name": "Nimi"
+ },
+ "description": "Seadista Ambee sidumine Home Assistantiga."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/he.json b/homeassistant/components/ambee/translations/he.json
new file mode 100644
index 00000000000..7b7882cd4df
--- /dev/null
+++ b/homeassistant/components/ambee/translations/he.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \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",
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
+ "name": "\u05e9\u05dd"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/hu.json b/homeassistant/components/ambee/translations/hu.json
new file mode 100644
index 00000000000..556412764a2
--- /dev/null
+++ b/homeassistant/components/ambee/translations/hu.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "api_key": "API kulcs"
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "API kulcs",
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g",
+ "name": "N\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/it.json b/homeassistant/components/ambee/translations/it.json
new file mode 100644
index 00000000000..178e52a979f
--- /dev/null
+++ b/homeassistant/components/ambee/translations/it.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_api_key": "Chiave API non valida"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "api_key": "Chiave API",
+ "description": "Riautenticati con il tuo account Ambee."
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "Chiave API",
+ "latitude": "Latitudine",
+ "longitude": "Logitudine",
+ "name": "Nome"
+ },
+ "description": "Configura Ambee per l'integrazione con Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/nl.json b/homeassistant/components/ambee/translations/nl.json
new file mode 100644
index 00000000000..837e39a72d7
--- /dev/null
+++ b/homeassistant/components/ambee/translations/nl.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Herauthenticatie was succesvol"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_api_key": "Ongeldige API-sleutel"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "api_key": "API-sleutel",
+ "description": "Verifieer opnieuw met uw Ambee-account."
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "API-sleutel",
+ "latitude": "Breedtegraad",
+ "longitude": "Lengtegraad",
+ "name": "Naam"
+ },
+ "description": "Stel Ambee in om te integreren met Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/no.json b/homeassistant/components/ambee/translations/no.json
new file mode 100644
index 00000000000..b735ee91509
--- /dev/null
+++ b/homeassistant/components/ambee/translations/no.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_api_key": "Ugyldig API-n\u00f8kkel"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "api_key": "API-n\u00f8kkel",
+ "description": "Autentiser p\u00e5 nytt med Ambee-kontoen din."
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "API-n\u00f8kkel",
+ "latitude": "Breddegrad",
+ "longitude": "Lengdegrad",
+ "name": "Navn"
+ },
+ "description": "Sett opp Ambee for \u00e5 integrere med Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/pl.json b/homeassistant/components/ambee/translations/pl.json
new file mode 100644
index 00000000000..d0b2225cc9a
--- /dev/null
+++ b/homeassistant/components/ambee/translations/pl.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_api_key": "Nieprawid\u0142owy klucz API"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "api_key": "Klucz API",
+ "description": "Ponownie uwierzytelnij za pomoc\u0105 konta Ambee."
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "Klucz API",
+ "latitude": "Szeroko\u015b\u0107 geograficzna",
+ "longitude": "D\u0142ugo\u015b\u0107 geograficzna",
+ "name": "Nazwa"
+ },
+ "description": "Skonfiguruj Ambee, aby zintegrowa\u0107 go z Home Assistantem."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/ru.json b/homeassistant/components/ambee/translations/ru.json
new file mode 100644
index 00000000000..5fb89879a4e
--- /dev/null
+++ b/homeassistant/components/ambee/translations/ru.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API."
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "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 Ambee."
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Ambee."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/sensor.ca.json b/homeassistant/components/ambee/translations/sensor.ca.json
new file mode 100644
index 00000000000..b85d6bdc8e2
--- /dev/null
+++ b/homeassistant/components/ambee/translations/sensor.ca.json
@@ -0,0 +1,10 @@
+{
+ "state": {
+ "ambee__risk": {
+ "high": "Alt",
+ "low": "Baix",
+ "moderate": "Moderat",
+ "very high": "Molt alt"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/sensor.de.json b/homeassistant/components/ambee/translations/sensor.de.json
new file mode 100644
index 00000000000..c96a2c50eb7
--- /dev/null
+++ b/homeassistant/components/ambee/translations/sensor.de.json
@@ -0,0 +1,10 @@
+{
+ "state": {
+ "ambee__risk": {
+ "high": "Hoch",
+ "low": "Niedrig",
+ "moderate": "M\u00e4\u00dfig",
+ "very high": "Sehr hoch"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/sensor.en.json b/homeassistant/components/ambee/translations/sensor.en.json
new file mode 100644
index 00000000000..a4b198eadf5
--- /dev/null
+++ b/homeassistant/components/ambee/translations/sensor.en.json
@@ -0,0 +1,10 @@
+{
+ "state": {
+ "ambee__risk": {
+ "high": "High",
+ "low": "Low",
+ "moderate": "Moderate",
+ "very high": "Very High"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/sensor.es.json b/homeassistant/components/ambee/translations/sensor.es.json
new file mode 100644
index 00000000000..a676ca7aa5e
--- /dev/null
+++ b/homeassistant/components/ambee/translations/sensor.es.json
@@ -0,0 +1,10 @@
+{
+ "state": {
+ "ambee__risk": {
+ "high": "Alto",
+ "low": "Bajo",
+ "moderate": "Moderado",
+ "very high": "Muy alto"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/sensor.et.json b/homeassistant/components/ambee/translations/sensor.et.json
new file mode 100644
index 00000000000..7599f2fd2c3
--- /dev/null
+++ b/homeassistant/components/ambee/translations/sensor.et.json
@@ -0,0 +1,10 @@
+{
+ "state": {
+ "ambee__risk": {
+ "high": "K\u00f5rge",
+ "low": "Madal",
+ "moderate": "M\u00f5\u00f5dukas",
+ "very high": "V\u00e4ga k\u00f5rge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/sensor.hu.json b/homeassistant/components/ambee/translations/sensor.hu.json
new file mode 100644
index 00000000000..aa86baf2722
--- /dev/null
+++ b/homeassistant/components/ambee/translations/sensor.hu.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "ambee__risk": {
+ "high": "Magas",
+ "low": "Alacsony",
+ "very high": "Nagyon magas"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/sensor.it.json b/homeassistant/components/ambee/translations/sensor.it.json
new file mode 100644
index 00000000000..1c265a6ca53
--- /dev/null
+++ b/homeassistant/components/ambee/translations/sensor.it.json
@@ -0,0 +1,10 @@
+{
+ "state": {
+ "ambee__risk": {
+ "high": "Alto",
+ "low": "Basso",
+ "moderate": "Moderato",
+ "very high": "Molto alto"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/sensor.nl.json b/homeassistant/components/ambee/translations/sensor.nl.json
new file mode 100644
index 00000000000..e9ba0c76a34
--- /dev/null
+++ b/homeassistant/components/ambee/translations/sensor.nl.json
@@ -0,0 +1,10 @@
+{
+ "state": {
+ "ambee__risk": {
+ "high": "Hoog",
+ "low": "Laag",
+ "moderate": "Matig",
+ "very high": "Zeer hoog"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/sensor.no.json b/homeassistant/components/ambee/translations/sensor.no.json
new file mode 100644
index 00000000000..cf4e4bed6ed
--- /dev/null
+++ b/homeassistant/components/ambee/translations/sensor.no.json
@@ -0,0 +1,10 @@
+{
+ "state": {
+ "ambee__risk": {
+ "high": "H\u00f8y",
+ "low": "Lav",
+ "moderate": "Moderat",
+ "very high": "Veldig h\u00f8y"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/sensor.pl.json b/homeassistant/components/ambee/translations/sensor.pl.json
new file mode 100644
index 00000000000..64d04cced48
--- /dev/null
+++ b/homeassistant/components/ambee/translations/sensor.pl.json
@@ -0,0 +1,10 @@
+{
+ "state": {
+ "ambee__risk": {
+ "high": "Wysoki",
+ "low": "Niski",
+ "moderate": "Umiarkowany",
+ "very high": "Bardzo wysoki"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/sensor.ru.json b/homeassistant/components/ambee/translations/sensor.ru.json
new file mode 100644
index 00000000000..c0dbe8cecd6
--- /dev/null
+++ b/homeassistant/components/ambee/translations/sensor.ru.json
@@ -0,0 +1,10 @@
+{
+ "state": {
+ "ambee__risk": {
+ "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439",
+ "low": "\u041d\u0438\u0437\u043a\u0438\u0439",
+ "moderate": "\u0423\u043c\u0435\u0440\u0435\u043d\u043d\u044b\u0439",
+ "very high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/sensor.zh-Hant.json b/homeassistant/components/ambee/translations/sensor.zh-Hant.json
new file mode 100644
index 00000000000..1e3c5bbe58d
--- /dev/null
+++ b/homeassistant/components/ambee/translations/sensor.zh-Hant.json
@@ -0,0 +1,10 @@
+{
+ "state": {
+ "ambee__risk": {
+ "high": "\u9ad8",
+ "low": "\u4f4e",
+ "moderate": "\u4e2d",
+ "very high": "\u6975\u9ad8"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambee/translations/zh-Hant.json b/homeassistant/components/ambee/translations/zh-Hant.json
new file mode 100644
index 00000000000..d2b53af8e5e
--- /dev/null
+++ b/homeassistant/components/ambee/translations/zh-Hant.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_api_key": "API \u5bc6\u9470\u7121\u6548"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "api_key": "API \u5bc6\u9470",
+ "description": "\u91cd\u65b0\u8a8d\u8b49 Ambee \u5e33\u865f\u3002"
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "API \u5bc6\u9470",
+ "latitude": "\u7def\u5ea6",
+ "longitude": "\u7d93\u5ea6",
+ "name": "\u540d\u7a31"
+ },
+ "description": "\u8a2d\u5b9a Ambee \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambiclimate/translations/he.json b/homeassistant/components/ambiclimate/translations/he.json
new file mode 100644
index 00000000000..7b7ec9c8c30
--- /dev/null
+++ b/homeassistant/components/ambiclimate/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3."
+ },
+ "create_entry": {
+ "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambient_station/translations/he.json b/homeassistant/components/ambient_station/translations/he.json
index f5afbca71c0..f34e568aa2f 100644
--- a/homeassistant/components/ambient_station/translations/he.json
+++ b/homeassistant/components/ambient_station/translations/he.json
@@ -1,6 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
"error": {
+ "invalid_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
"no_devices": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05df \u05d1\u05d7\u05e9\u05d1\u05d5\u05df"
},
"step": {
diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json
index 9ab02fec68a..d1e379435a0 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.3.1",
- "androidtv[async]==0.0.59",
+ "adb-shell[async]==0.3.4",
+ "androidtv[async]==0.0.60",
"pure-python-adb[async]==0.3.0.dev0"
],
"codeowners": ["@JeffLIrion"],
diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py
index a1bd50ab221..e8b900d8213 100644
--- a/homeassistant/components/apple_tv/__init__.py
+++ b/homeassistant/components/apple_tv/__init__.py
@@ -270,7 +270,7 @@ class AppleTVManager:
self.hass.components.persistent_notification.create(
"An irrecoverable connection problem occurred when connecting to "
- f"`f{name}`. Please go to the Integrations page and reconfigure it",
+ f"`{name}`. Please go to the Integrations page and reconfigure it",
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID,
)
diff --git a/homeassistant/components/apple_tv/translations/de.json b/homeassistant/components/apple_tv/translations/de.json
index 464bad99d5a..6161550d736 100644
--- a/homeassistant/components/apple_tv/translations/de.json
+++ b/homeassistant/components/apple_tv/translations/de.json
@@ -16,7 +16,7 @@
"no_usable_service": "Es wurde ein Ger\u00e4t gefunden, aber es konnte keine M\u00f6glichkeit gefunden werden, eine Verbindung zu diesem Ger\u00e4t herzustellen. Wenn diese Meldung weiterhin erscheint, versuche, die IP-Adresse anzugeben oder den Apple TV neu zu starten.",
"unknown": "Unerwarteter Fehler"
},
- "flow_title": "Apple TV: {name}",
+ "flow_title": "{name}",
"step": {
"confirm": {
"description": "Es wird der Apple TV mit dem Namen \" {name} \" zu Home Assistant hinzugef\u00fcgt. \n\n ** Um den Vorgang abzuschlie\u00dfen, m\u00fcssen m\u00f6glicherweise mehrere PIN-Codes eingegeben werden. ** \n\n Bitte beachte, dass der Apple TV mit dieser Integration * nicht * ausgeschalten werden kann. Nur der Media Player in Home Assistant wird ausgeschaltet!",
diff --git a/homeassistant/components/apple_tv/translations/he.json b/homeassistant/components/apple_tv/translations/he.json
new file mode 100644
index 00000000000..81e13ec1878
--- /dev/null
+++ b/homeassistant/components/apple_tv/translations/he.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea",
+ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "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}",
+ "step": {
+ "pair_with_pin": {
+ "data": {
+ "pin": "\u05e7\u05d5\u05d3 PIN"
+ }
+ },
+ "user": {
+ "data": {
+ "device_input": "\u05d4\u05ea\u05e7\u05df"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json
index 63bf29a73f1..72b334849a0 100644
--- a/homeassistant/components/apple_tv/translations/hu.json
+++ b/homeassistant/components/apple_tv/translations/hu.json
@@ -12,7 +12,7 @@
"no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "Apple TV: {name}",
+ "flow_title": "{name}",
"step": {
"confirm": {
"title": "Apple TV sikeresen hozz\u00e1adva"
diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py
index e1dfac09d76..905e31c798b 100644
--- a/homeassistant/components/arcam_fmj/__init__.py
+++ b/homeassistant/components/arcam_fmj/__init__.py
@@ -7,7 +7,7 @@ from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
import async_timeout
-from homeassistant import config_entries
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
@@ -51,7 +51,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up config entry."""
entries = hass.data[DOMAIN_DATA_ENTRIES]
tasks = hass.data[DOMAIN_DATA_TASKS]
diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py
index 4ae34abb2c2..383f28d7a20 100644
--- a/homeassistant/components/arcam_fmj/device_trigger.py
+++ b/homeassistant/components/arcam_fmj/device_trigger.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_DEVICE_ID,
@@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, EVENT_TURN_ON
TRIGGER_TYPES = {"turn_on"}
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
@@ -56,7 +56,7 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
job = HassJob(action)
if config[CONF_TYPE] == "turn_on":
@@ -69,9 +69,9 @@ async def async_attach_trigger(
job,
{
"trigger": {
+ **trigger_data,
**config,
"description": f"{DOMAIN} - {entity_id}",
- "id": trigger_id,
}
},
event.context,
diff --git a/homeassistant/components/arcam_fmj/strings.json b/homeassistant/components/arcam_fmj/strings.json
index 154727baf9f..435a6971d5b 100644
--- a/homeassistant/components/arcam_fmj/strings.json
+++ b/homeassistant/components/arcam_fmj/strings.json
@@ -5,7 +5,6 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
- "error": {},
"flow_title": "{host}",
"step": {
"confirm": {
diff --git a/homeassistant/components/arcam_fmj/translations/de.json b/homeassistant/components/arcam_fmj/translations/de.json
index 1f67a8d30a9..684a2f18961 100644
--- a/homeassistant/components/arcam_fmj/translations/de.json
+++ b/homeassistant/components/arcam_fmj/translations/de.json
@@ -5,7 +5,7 @@
"already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
"cannot_connect": "Verbindung fehlgeschlagen"
},
- "flow_title": "Arcam FMJ auf {host}",
+ "flow_title": "{host}",
"step": {
"confirm": {
"description": "M\u00f6chtest du Arcam FMJ auf `{host}` zum Home Assistant hinzuf\u00fcgen?"
diff --git a/homeassistant/components/arcam_fmj/translations/he.json b/homeassistant/components/arcam_fmj/translations/he.json
new file mode 100644
index 00000000000..0a4bd9ca12a
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/translations/he.json
@@ -0,0 +1,22 @@
+{
+ "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",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{host}",
+ "step": {
+ "confirm": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea Arcam FMJ \u05d1- '{host}' \u05dc-Home Assistant?"
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ },
+ "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4-IP \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json
index 4af1181a265..e1784c4ad66 100644
--- a/homeassistant/components/arcam_fmj/translations/hu.json
+++ b/homeassistant/components/arcam_fmj/translations/hu.json
@@ -5,6 +5,7 @@
"already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
+ "flow_title": "{host}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/arcam_fmj/translations/it.json b/homeassistant/components/arcam_fmj/translations/it.json
index 24c9b99e7a8..2b99566888b 100644
--- a/homeassistant/components/arcam_fmj/translations/it.json
+++ b/homeassistant/components/arcam_fmj/translations/it.json
@@ -5,6 +5,10 @@
"already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"cannot_connect": "Impossibile connettersi"
},
+ "error": {
+ "one": "Pi\u00f9",
+ "other": "Altri"
+ },
"flow_title": "{host}",
"step": {
"confirm": {
diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py
index ad3cea1106b..af7f3b05e33 100644
--- a/homeassistant/components/asuswrt/__init__.py
+++ b/homeassistant/components/asuswrt/__init__.py
@@ -111,7 +111,7 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up AsusWrt platform."""
# import options from yaml if empty
diff --git a/homeassistant/components/asuswrt/const.py b/homeassistant/components/asuswrt/const.py
index a8977a77ea8..e41d683a7df 100644
--- a/homeassistant/components/asuswrt/const.py
+++ b/homeassistant/components/asuswrt/const.py
@@ -21,8 +21,7 @@ PROTOCOL_SSH = "ssh"
PROTOCOL_TELNET = "telnet"
# Sensors
-SENSOR_CONNECTED_DEVICE = "sensor_connected_device"
-SENSOR_RX_BYTES = "sensor_rx_bytes"
-SENSOR_TX_BYTES = "sensor_tx_bytes"
-SENSOR_RX_RATES = "sensor_rx_rates"
-SENSOR_TX_RATES = "sensor_tx_rates"
+SENSORS_BYTES = ["sensor_rx_bytes", "sensor_tx_bytes"]
+SENSORS_CONNECTED_DEVICE = ["sensor_connected_device"]
+SENSORS_LOAD_AVG = ["sensor_load_avg1", "sensor_load_avg5", "sensor_load_avg15"]
+SENSORS_RATES = ["sensor_rx_rates", "sensor_tx_rates"]
diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py
index 134cf960aae..3c911d7712e 100644
--- a/homeassistant/components/asuswrt/router.py
+++ b/homeassistant/components/asuswrt/router.py
@@ -40,11 +40,10 @@ from .const import (
DEFAULT_TRACK_UNKNOWN,
DOMAIN,
PROTOCOL_TELNET,
- SENSOR_CONNECTED_DEVICE,
- SENSOR_RX_BYTES,
- SENSOR_RX_RATES,
- SENSOR_TX_BYTES,
- SENSOR_TX_RATES,
+ SENSORS_BYTES,
+ SENSORS_CONNECTED_DEVICE,
+ SENSORS_LOAD_AVG,
+ SENSORS_RATES,
)
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
@@ -56,11 +55,22 @@ SCAN_INTERVAL = timedelta(seconds=30)
SENSORS_TYPE_BYTES = "sensors_bytes"
SENSORS_TYPE_COUNT = "sensors_count"
+SENSORS_TYPE_LOAD_AVG = "sensors_load_avg"
SENSORS_TYPE_RATES = "sensors_rates"
_LOGGER = logging.getLogger(__name__)
+def _get_dict(keys: list, values: list) -> dict[str, Any]:
+ """Create a dict from a list of keys and values."""
+ ret_dict: dict[str, Any] = dict.fromkeys(keys)
+
+ for index, key in enumerate(ret_dict):
+ ret_dict[key] = values[index]
+
+ return ret_dict
+
+
class AsusWrtSensorDataHandler:
"""Data handler for AsusWrt sensor."""
@@ -72,33 +82,34 @@ class AsusWrtSensorDataHandler:
async def _get_connected_devices(self):
"""Return number of connected devices."""
- return {SENSOR_CONNECTED_DEVICE: self._connected_devices}
+ return {SENSORS_CONNECTED_DEVICE[0]: self._connected_devices}
async def _get_bytes(self):
"""Fetch byte information from the router."""
- ret_dict: dict[str, Any] = {}
try:
datas = await self._api.async_get_bytes_total()
- except OSError as exc:
- raise UpdateFailed from exc
+ except (OSError, ValueError) as exc:
+ raise UpdateFailed(exc) from exc
- ret_dict[SENSOR_RX_BYTES] = datas[0]
- ret_dict[SENSOR_TX_BYTES] = datas[1]
-
- return ret_dict
+ return _get_dict(SENSORS_BYTES, datas)
async def _get_rates(self):
"""Fetch rates information from the router."""
- ret_dict: dict[str, Any] = {}
try:
rates = await self._api.async_get_current_transfer_rates()
- except OSError as exc:
- raise UpdateFailed from exc
+ except (OSError, ValueError) as exc:
+ raise UpdateFailed(exc) from exc
- ret_dict[SENSOR_RX_RATES] = rates[0]
- ret_dict[SENSOR_TX_RATES] = rates[1]
+ return _get_dict(SENSORS_RATES, rates)
- return ret_dict
+ async def _get_load_avg(self):
+ """Fetch load average information from the router."""
+ try:
+ avg = await self._api.async_get_loadavg()
+ except (OSError, ValueError) as exc:
+ raise UpdateFailed(exc) from exc
+
+ return _get_dict(SENSORS_LOAD_AVG, avg)
def update_device_count(self, conn_devices: int):
"""Update connected devices attribute."""
@@ -113,6 +124,8 @@ class AsusWrtSensorDataHandler:
method = self._get_connected_devices
elif sensor_type == SENSORS_TYPE_BYTES:
method = self._get_bytes
+ elif sensor_type == SENSORS_TYPE_LOAD_AVG:
+ method = self._get_load_avg
elif sensor_type == SENSORS_TYPE_RATES:
method = self._get_rates
else:
@@ -315,29 +328,21 @@ class AsusWrtRouter:
self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api)
self._sensors_data_handler.update_device_count(self._connected_devices)
- conn_dev_coordinator = await self._sensors_data_handler.get_coordinator(
- SENSORS_TYPE_COUNT, False
- )
- self._sensors_coordinator[SENSORS_TYPE_COUNT] = {
- KEY_COORDINATOR: conn_dev_coordinator,
- KEY_SENSORS: [SENSOR_CONNECTED_DEVICE],
+ sensors_types = {
+ SENSORS_TYPE_BYTES: SENSORS_BYTES,
+ SENSORS_TYPE_COUNT: SENSORS_CONNECTED_DEVICE,
+ SENSORS_TYPE_LOAD_AVG: SENSORS_LOAD_AVG,
+ SENSORS_TYPE_RATES: SENSORS_RATES,
}
- bytes_coordinator = await self._sensors_data_handler.get_coordinator(
- SENSORS_TYPE_BYTES
- )
- self._sensors_coordinator[SENSORS_TYPE_BYTES] = {
- KEY_COORDINATOR: bytes_coordinator,
- KEY_SENSORS: [SENSOR_RX_BYTES, SENSOR_TX_BYTES],
- }
-
- rates_coordinator = await self._sensors_data_handler.get_coordinator(
- SENSORS_TYPE_RATES
- )
- self._sensors_coordinator[SENSORS_TYPE_RATES] = {
- KEY_COORDINATOR: rates_coordinator,
- KEY_SENSORS: [SENSOR_RX_RATES, SENSOR_TX_RATES],
- }
+ for sensor_type, sensor_names in sensors_types.items():
+ coordinator = await self._sensors_data_handler.get_coordinator(
+ sensor_type, sensor_type != SENSORS_TYPE_COUNT
+ )
+ self._sensors_coordinator[sensor_type] = {
+ KEY_COORDINATOR: coordinator,
+ KEY_SENSORS: sensor_names,
+ }
async def _update_unpolled_sensors(self) -> None:
"""Request refresh for AsusWrt unpolled sensors."""
@@ -419,12 +424,12 @@ class AsusWrtRouter:
return self._api
-async def _get_nvram_info(api: AsusWrt, info_type):
+async def _get_nvram_info(api: AsusWrt, info_type: str) -> dict[str, Any]:
"""Get AsusWrt router info from nvram."""
info = {}
try:
info = await api.async_get_nvram(info_type)
- except OSError as exc:
+ except (OSError, UnicodeDecodeError) as exc:
_LOGGER.warning("Error calling method async_get_nvram(%s): %s", info_type, exc)
return info
diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py
index 6ec077620f6..086c7373a4e 100644
--- a/homeassistant/components/asuswrt/sensor.py
+++ b/homeassistant/components/asuswrt/sensor.py
@@ -18,11 +18,10 @@ from homeassistant.helpers.update_coordinator import (
from .const import (
DATA_ASUSWRT,
DOMAIN,
- SENSOR_CONNECTED_DEVICE,
- SENSOR_RX_BYTES,
- SENSOR_RX_RATES,
- SENSOR_TX_BYTES,
- SENSOR_TX_RATES,
+ SENSORS_BYTES,
+ SENSORS_CONNECTED_DEVICE,
+ SENSORS_LOAD_AVG,
+ SENSORS_RATES,
)
from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter
@@ -38,41 +37,48 @@ SENSOR_DEFAULT_ENABLED = "default_enabled"
UNIT_DEVICES = "Devices"
CONNECTION_SENSORS = {
- SENSOR_CONNECTED_DEVICE: {
+ SENSORS_CONNECTED_DEVICE[0]: {
SENSOR_NAME: "Devices Connected",
SENSOR_UNIT: UNIT_DEVICES,
SENSOR_FACTOR: 0,
SENSOR_ICON: "mdi:router-network",
- SENSOR_DEVICE_CLASS: None,
SENSOR_DEFAULT_ENABLED: True,
},
- SENSOR_RX_RATES: {
+ SENSORS_RATES[0]: {
SENSOR_NAME: "Download Speed",
SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND,
SENSOR_FACTOR: 125000,
SENSOR_ICON: "mdi:download-network",
- SENSOR_DEVICE_CLASS: None,
},
- SENSOR_TX_RATES: {
+ SENSORS_RATES[1]: {
SENSOR_NAME: "Upload Speed",
SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND,
SENSOR_FACTOR: 125000,
SENSOR_ICON: "mdi:upload-network",
- SENSOR_DEVICE_CLASS: None,
},
- SENSOR_RX_BYTES: {
+ SENSORS_BYTES[0]: {
SENSOR_NAME: "Download",
SENSOR_UNIT: DATA_GIGABYTES,
SENSOR_FACTOR: 1000000000,
SENSOR_ICON: "mdi:download",
- SENSOR_DEVICE_CLASS: None,
},
- SENSOR_TX_BYTES: {
+ SENSORS_BYTES[1]: {
SENSOR_NAME: "Upload",
SENSOR_UNIT: DATA_GIGABYTES,
SENSOR_FACTOR: 1000000000,
SENSOR_ICON: "mdi:upload",
- SENSOR_DEVICE_CLASS: None,
+ },
+ SENSORS_LOAD_AVG[0]: {
+ SENSOR_NAME: "Load Avg (1m)",
+ SENSOR_ICON: "mdi:cpu-32-bit",
+ },
+ SENSORS_LOAD_AVG[1]: {
+ SENSOR_NAME: "Load Avg (5m)",
+ SENSOR_ICON: "mdi:cpu-32-bit",
+ },
+ SENSORS_LOAD_AVG[2]: {
+ SENSOR_NAME: "Load Avg (15m)",
+ SENSOR_ICON: "mdi:cpu-32-bit",
},
}
@@ -108,24 +114,21 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity):
coordinator: DataUpdateCoordinator,
router: AsusWrtRouter,
sensor_type: str,
- sensor: dict[str, Any],
+ sensor_def: dict[str, Any],
) -> None:
"""Initialize a AsusWrt sensor."""
super().__init__(coordinator)
self._router = router
self._sensor_type = sensor_type
- self._name = f"{DEFAULT_PREFIX} {sensor[SENSOR_NAME]}"
+ self._sensor_def = sensor_def
+ self._name = f"{DEFAULT_PREFIX} {sensor_def[SENSOR_NAME]}"
self._unique_id = f"{DOMAIN} {self._name}"
- self._unit = sensor[SENSOR_UNIT]
- self._factor = sensor[SENSOR_FACTOR]
- self._icon = sensor[SENSOR_ICON]
- self._device_class = sensor[SENSOR_DEVICE_CLASS]
- self._default_enabled = sensor.get(SENSOR_DEFAULT_ENABLED, False)
+ self._factor = sensor_def.get(SENSOR_FACTOR)
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
- return self._default_enabled
+ return self._sensor_def.get(SENSOR_DEFAULT_ENABLED, False)
@property
def state(self) -> str:
@@ -150,17 +153,17 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity):
@property
def unit_of_measurement(self) -> str:
"""Return the unit."""
- return self._unit
+ return self._sensor_def.get(SENSOR_UNIT)
@property
def icon(self) -> str:
"""Return the icon."""
- return self._icon
+ return self._sensor_def.get(SENSOR_ICON)
@property
def device_class(self) -> str:
"""Return the device_class."""
- return self._device_class
+ return self._sensor_def.get(SENSOR_DEVICE_CLASS)
@property
def extra_state_attributes(self) -> dict[str, Any]:
diff --git a/homeassistant/components/asuswrt/translations/de.json b/homeassistant/components/asuswrt/translations/de.json
index 36699d95753..bf7e2230810 100644
--- a/homeassistant/components/asuswrt/translations/de.json
+++ b/homeassistant/components/asuswrt/translations/de.json
@@ -23,6 +23,7 @@
"ssh_key": "Pfad zu deiner SSH-Schl\u00fcsseldatei (anstelle des Passworts)",
"username": "Benutzername"
},
+ "description": "Einstellen der erforderlichen Parameter f\u00fcr die Verbindung mit Ihrem Router.",
"title": ""
}
}
@@ -31,8 +32,11 @@
"step": {
"init": {
"data": {
+ "consider_home": "Sekunden, um ein Ger\u00e4t als 'abwesend' zu betrachten",
+ "dnsmasq": "Der Speicherort der dnsmasq.leases-Dateien im Router",
"interface": "Schnittstelle, von der du Statistiken haben m\u00f6chtest (z.B. eth0, eth1 usw.)",
- "require_ip": "Ger\u00e4te m\u00fcssen IP haben (f\u00fcr Zugangspunkt-Modus)"
+ "require_ip": "Ger\u00e4te m\u00fcssen IP haben (f\u00fcr Zugangspunkt-Modus)",
+ "track_unknown": "Unbekannte / unbenannte Ger\u00e4te tracken"
},
"title": "AsusWRT Optionen"
}
diff --git a/homeassistant/components/asuswrt/translations/he.json b/homeassistant/components/asuswrt/translations/he.json
new file mode 100644
index 00000000000..7b859e5af0c
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/he.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea."
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd",
+ "pwd_and_ssh": "\u05e1\u05e4\u05e7 \u05e8\u05e7 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d0\u05d5 \u05e7\u05d5\u05d1\u05e5 \u05de\u05e4\u05ea\u05d7 SSH",
+ "pwd_or_ssh": "\u05d0\u05e0\u05d0 \u05e1\u05e4\u05e7 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d0\u05d5 \u05e7\u05d5\u05d1\u05e5 \u05de\u05e4\u05ea\u05d7 SSH",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "mode": "\u05de\u05e6\u05d1",
+ "name": "\u05e9\u05dd",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "ssh_key": "\u05e0\u05ea\u05d9\u05d1 \u05dc\u05e7\u05d5\u05d1\u05e5 \u05d4\u05de\u05e4\u05ea\u05d7 \u05e9\u05dc SSH (\u05d1\u05de\u05e7\u05d5\u05dd \u05dc\u05e1\u05d9\u05e1\u05de\u05d4)",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py
index e6347563bc2..af5eff67f57 100644
--- a/homeassistant/components/atag/__init__.py
+++ b/homeassistant/components/atag/__init__.py
@@ -24,7 +24,7 @@ DOMAIN = "atag"
PLATFORMS = [CLIMATE, WATER_HEATER, SENSOR]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Atag integration from a config entry."""
async def _async_update_data():
diff --git a/homeassistant/components/atag/translations/he.json b/homeassistant/components/atag/translations/he.json
new file mode 100644
index 00000000000..c3a67844fdd
--- /dev/null
+++ b/homeassistant/components/atag/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/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py
index 1bf54085064..43146938961 100644
--- a/homeassistant/components/aten_pe/switch.py
+++ b/homeassistant/components/aten_pe/switch.py
@@ -67,6 +67,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class AtenSwitch(SwitchEntity):
"""Represents an ATEN PE switch."""
+ _attr_device_class = DEVICE_CLASS_OUTLET
+
def __init__(self, device, mac, outlet, name):
"""Initialize an ATEN PE switch."""
self._device = device
@@ -86,11 +88,6 @@ class AtenSwitch(SwitchEntity):
"""Return the name of the entity."""
return self._name
- @property
- def device_class(self) -> str:
- """Return the class of this device, from component DEVICE_CLASSES."""
- return DEVICE_CLASS_OUTLET
-
@property
def is_on(self) -> bool:
"""Return True if entity is on."""
diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py
index 285b6c70713..bcb7b4f1ece 100644
--- a/homeassistant/components/atome/sensor.py
+++ b/homeassistant/components/atome/sensor.py
@@ -5,7 +5,11 @@ import logging
from pyatome.client import AtomeClient, PyAtomeError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.components.sensor import (
+ PLATFORM_SCHEMA,
+ STATE_CLASS_MEASUREMENT,
+ SensorEntity,
+)
from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
@@ -39,8 +43,6 @@ WEEKLY_TYPE = "week"
MONTHLY_TYPE = "month"
YEARLY_TYPE = "year"
-ICON = "mdi:flash"
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_USERNAME): cv.string,
@@ -217,49 +219,20 @@ class AtomeData:
class AtomeSensor(SensorEntity):
"""Representation of a sensor entity for Atome."""
+ _attr_device_class = DEVICE_CLASS_POWER
+
def __init__(self, data, name, sensor_type):
"""Initialize the sensor."""
- self._name = name
+ self._attr_name = name
self._data = data
- self._state = None
- self._attributes = {}
self._sensor_type = sensor_type
if sensor_type == LIVE_TYPE:
- self._unit_of_measurement = POWER_WATT
+ self._attr_unit_of_measurement = POWER_WATT
+ self._attr_state_class = STATE_CLASS_MEASUREMENT
else:
- self._unit_of_measurement = ENERGY_KILO_WATT_HOUR
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def extra_state_attributes(self):
- """Return the state attributes."""
- return self._attributes
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return self._unit_of_measurement
-
- @property
- def icon(self):
- """Icon to use in the frontend, if any."""
- return ICON
-
- @property
- def device_class(self):
- """Return the device class."""
- return DEVICE_CLASS_POWER
+ self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR
def update(self):
"""Update device state."""
@@ -267,11 +240,13 @@ class AtomeSensor(SensorEntity):
update_function()
if self._sensor_type == LIVE_TYPE:
- self._state = self._data.live_power
- self._attributes["subscribed_power"] = self._data.subscribed_power
- self._attributes["is_connected"] = self._data.is_connected
+ self._attr_state = self._data.live_power
+ self._attr_extra_state_attributes = {
+ "subscribed_power": self._data.subscribed_power,
+ "is_connected": self._data.is_connected,
+ }
else:
- self._state = getattr(self._data, f"{self._sensor_type}_usage")
- self._attributes["price"] = getattr(
- self._data, f"{self._sensor_type}_price"
- )
+ self._attr_state = getattr(self._data, f"{self._sensor_type}_usage")
+ self._attr_extra_state_attributes = {
+ "price": getattr(self._data, f"{self._sensor_type}_price")
+ }
diff --git a/homeassistant/components/august/translations/he.json b/homeassistant/components/august/translations/he.json
new file mode 100644
index 00000000000..5fb689b2562
--- /dev/null
+++ b/homeassistant/components/august/translations/he.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "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",
+ "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": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 {username} .",
+ "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d7\u05e9\u05d1\u05d5\u05df August"
+ },
+ "user_validate": {
+ "data": {
+ "login_method": "\u05e9\u05d9\u05d8\u05ea \u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "validation": {
+ "description": "\u05d0\u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea {login_method} \u05e9\u05dc\u05da ({\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9}) \u05d5\u05d4\u05d6\u05df \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05dc\u05d4\u05dc\u05df"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py
index e8dc98a18b7..faccefda500 100644
--- a/homeassistant/components/aurora/__init__.py
+++ b/homeassistant/components/aurora/__init__.py
@@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["binary_sensor", "sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Aurora from a config entry."""
conf = entry.data
diff --git a/homeassistant/components/aurora/translations/he.json b/homeassistant/components/aurora/translations/he.json
new file mode 100644
index 00000000000..a11e0a72254
--- /dev/null
+++ b/homeassistant/components/aurora/translations/he.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
+ "name": "\u05e9\u05dd"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py
index f4640e7c014..cd4a71d1b31 100644
--- a/homeassistant/components/aurora_abb_powerone/sensor.py
+++ b/homeassistant/components/aurora_abb_powerone/sensor.py
@@ -5,7 +5,11 @@ import logging
from aurorapy.client import AuroraError, AuroraSerialClient
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.components.sensor import (
+ PLATFORM_SCHEMA,
+ STATE_CLASS_MEASUREMENT,
+ SensorEntity,
+)
from homeassistant.const import (
CONF_ADDRESS,
CONF_DEVICE,
@@ -46,6 +50,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class AuroraABBSolarPVMonitorSensor(SensorEntity):
"""Representation of a Sensor."""
+ _attr_state_class = STATE_CLASS_MEASUREMENT
+
def __init__(self, client, name, typename):
"""Initialize the sensor."""
self._name = f"{name} {typename}"
diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py
index 725450a0a12..c951e652356 100644
--- a/homeassistant/components/auth/login_flow.py
+++ b/homeassistant/components/auth/login_flow.py
@@ -52,7 +52,7 @@ flow for details.
Progress the flow. Most flows will be 1 page, but could optionally add extra
login challenges, like TFA. Once the flow has finished, the returned step will
-have type "create_entry" and "result" key will contain an authorization code.
+have type RESULT_TYPE_CREATE_ENTRY and "result" key will contain an authorization code.
The authorization code associated with an authorized user by default, it will
associate with an credential if "type" set to "link_user" in
"/auth/login_flow"
diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py
index eb77880687d..1733f272229 100644
--- a/homeassistant/components/automation/__init__.py
+++ b/homeassistant/components/automation/__init__.py
@@ -475,8 +475,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
automation_trace.set_trigger_description(trigger_description)
# Add initial variables as the trigger step
- if "trigger" in variables and "id" in variables["trigger"]:
- trigger_path = f"trigger/{variables['trigger']['id']}"
+ if "trigger" in variables and "idx" in variables["trigger"]:
+ trigger_path = f"trigger/{variables['trigger']['idx']}"
else:
trigger_path = "trigger"
trace_element = TraceElement(variables, trigger_path)
diff --git a/homeassistant/components/awair/translations/he.json b/homeassistant/components/awair/translations/he.json
new file mode 100644
index 00000000000..55e8b21a52b
--- /dev/null
+++ b/homeassistant/components/awair/translations/he.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4",
+ "email": "\u05d3\u05d5\u05d0\"\u05dc"
+ }
+ },
+ "user": {
+ "data": {
+ "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4",
+ "email": "\u05d3\u05d5\u05d0\"\u05dc"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py
index 3e2b1a48eb7..a652aeb6df8 100644
--- a/homeassistant/components/axis/axis_base.py
+++ b/homeassistant/components/axis/axis_base.py
@@ -1,5 +1,6 @@
"""Base classes for Axis entities."""
+from homeassistant.const import ATTR_IDENTIFIERS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
@@ -14,6 +15,8 @@ class AxisEntityBase(Entity):
"""Initialize the Axis event."""
self.device = device
+ self._attr_device_info = {ATTR_IDENTIFIERS: {(AXIS_DOMAIN, device.unique_id)}}
+
async def async_added_to_hass(self):
"""Subscribe device events."""
self.async_on_remove(
@@ -27,11 +30,6 @@ class AxisEntityBase(Entity):
"""Return True if device is available."""
return self.device.available
- @property
- def device_info(self):
- """Return a device description for device registry."""
- return {"identifiers": {(AXIS_DOMAIN, self.device.unique_id)}}
-
@callback
def update_callback(self, no_delay=None):
"""Update the entities state."""
@@ -41,11 +39,18 @@ class AxisEntityBase(Entity):
class AxisEventBase(AxisEntityBase):
"""Base common to all Axis entities from event stream."""
+ _attr_should_poll = False
+
def __init__(self, event, device):
"""Initialize the Axis event."""
super().__init__(device)
self.event = event
+ self._attr_name = f"{device.name} {event.TYPE} {event.id}"
+ self._attr_unique_id = f"{device.unique_id}-{event.topic}-{event.id}"
+
+ self._attr_device_class = event.CLASS
+
async def async_added_to_hass(self) -> None:
"""Subscribe sensors events."""
self.event.register_callback(self.update_callback)
@@ -54,23 +59,3 @@ class AxisEventBase(AxisEntityBase):
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
self.event.remove_callback(self.update_callback)
-
- @property
- def device_class(self):
- """Return the class of the event."""
- return self.event.CLASS
-
- @property
- def name(self):
- """Return the name of the event."""
- return f"{self.device.name} {self.event.TYPE} {self.event.id}"
-
- @property
- def should_poll(self):
- """No polling needed."""
- return False
-
- @property
- def unique_id(self):
- """Return a unique identifier for this device."""
- return f"{self.device.unique_id}-{self.event.topic}-{self.event.id}"
diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py
index 222a356d4f9..7ef3838b1f7 100644
--- a/homeassistant/components/axis/binary_sensor.py
+++ b/homeassistant/components/axis/binary_sensor.py
@@ -66,6 +66,8 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity):
super().__init__(event, device)
self.cancel_scheduled_update = None
+ self._attr_device_class = DEVICE_CLASS.get(self.event.CLASS)
+
@callback
def update_callback(self, no_delay=False):
"""Update the sensor's state, if needed.
@@ -126,9 +128,4 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity):
):
return f"{self.device.name} {self.event.TYPE} {event_data[self.event.id].name}"
- return super().name
-
- @property
- def device_class(self):
- """Return the class of the sensor."""
- return DEVICE_CLASS.get(self.event.CLASS)
+ return self._attr_name
diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py
index cf2634b8f3a..bd0cd46a181 100644
--- a/homeassistant/components/axis/camera.py
+++ b/homeassistant/components/axis/camera.py
@@ -51,6 +51,8 @@ class AxisCamera(AxisEntityBase, MjpegCamera):
}
MjpegCamera.__init__(self, config)
+ self._attr_unique_id = f"{device.unique_id}-camera"
+
async def async_added_to_hass(self):
"""Subscribe camera events."""
self.async_on_remove(
@@ -71,11 +73,6 @@ class AxisCamera(AxisEntityBase, MjpegCamera):
self._mjpeg_url = self.mjpeg_source
self._still_image_url = self.image_source
- @property
- def unique_id(self) -> str:
- """Return a unique identifier for this device."""
- return f"{self.device.unique_id}-camera"
-
@property
def image_source(self) -> str:
"""Return still image URL for device."""
diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py
index e627d6ccdbd..ced795882e1 100644
--- a/homeassistant/components/axis/light.py
+++ b/homeassistant/components/axis/light.py
@@ -4,7 +4,7 @@ from axis.event_stream import CLASS_LIGHT
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
- SUPPORT_BRIGHTNESS,
+ COLOR_MODE_BRIGHTNESS,
LightEntity,
)
from homeassistant.core import callback
@@ -40,6 +40,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AxisLight(AxisEventBase, LightEntity):
"""Representation of a light Axis event."""
+ _attr_should_poll = True
+
def __init__(self, event, device):
"""Initialize the Axis light."""
super().__init__(event, device)
@@ -49,7 +51,11 @@ class AxisLight(AxisEventBase, LightEntity):
self.current_intensity = 0
self.max_intensity = 0
- self._features = SUPPORT_BRIGHTNESS
+ light_type = device.api.vapix.light_control[self.light_id].light_type
+ self._attr_name = f"{device.name} {light_type} {event.TYPE} {event.id}"
+
+ self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS}
+ self._attr_color_mode = COLOR_MODE_BRIGHTNESS
async def async_added_to_hass(self) -> None:
"""Subscribe lights events."""
@@ -67,17 +73,6 @@ class AxisLight(AxisEventBase, LightEntity):
)
self.max_intensity = max_intensity["data"]["ranges"][0]["high"]
- @property
- def supported_features(self):
- """Flag supported features."""
- return self._features
-
- @property
- def name(self):
- """Return the name of the light."""
- light_type = self.device.api.vapix.light_control[self.light_id].light_type
- return f"{self.device.name} {light_type} {self.event.TYPE} {self.event.id}"
-
@property
def is_on(self):
"""Return true if light is on."""
@@ -112,8 +107,3 @@ class AxisLight(AxisEventBase, LightEntity):
)
)
self.current_intensity = current_intensity["data"]["intensity"]
-
- @property
- def should_poll(self):
- """Brightness needs polling."""
- return True
diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py
index e509716fc1f..3a23c3202df 100644
--- a/homeassistant/components/axis/switch.py
+++ b/homeassistant/components/axis/switch.py
@@ -30,6 +30,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AxisSwitch(AxisEventBase, SwitchEntity):
"""Representation of a Axis switch."""
+ def __init__(self, event, device):
+ """Initialize the Axis switch."""
+ super().__init__(event, device)
+
+ if event.id and device.api.vapix.ports[event.id].name:
+ self._attr_name = f"{device.name} {device.api.vapix.ports[event.id].name}"
+
@property
def is_on(self):
"""Return true if event is active."""
@@ -42,13 +49,3 @@ class AxisSwitch(AxisEventBase, SwitchEntity):
async def async_turn_off(self, **kwargs):
"""Turn off switch."""
await self.device.api.vapix.ports[self.event.id].open()
-
- @property
- def name(self):
- """Return the name of the event."""
- if self.event.id and self.device.api.vapix.ports[self.event.id].name:
- return (
- f"{self.device.name} {self.device.api.vapix.ports[self.event.id].name}"
- )
-
- return super().name
diff --git a/homeassistant/components/axis/translations/de.json b/homeassistant/components/axis/translations/de.json
index ed95dea6fc1..607000b6eaa 100644
--- a/homeassistant/components/axis/translations/de.json
+++ b/homeassistant/components/axis/translations/de.json
@@ -11,7 +11,7 @@
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung"
},
- "flow_title": "Achsenger\u00e4t: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/translations/he.json b/homeassistant/components/axis/translations/he.json
index 3007c0e968c..903656d41cf 100644
--- a/homeassistant/components/axis/translations/he.json
+++ b/homeassistant/components/axis/translations/he.json
@@ -1,9 +1,22 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "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",
+ "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"
+ },
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
- "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
}
diff --git a/homeassistant/components/azure_devops/translations/de.json b/homeassistant/components/azure_devops/translations/de.json
index 43a5776da2e..eabc88625fb 100644
--- a/homeassistant/components/azure_devops/translations/de.json
+++ b/homeassistant/components/azure_devops/translations/de.json
@@ -9,7 +9,7 @@
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"project_error": "Konnte keine Projektinformationen erhalten."
},
- "flow_title": "Azure DevOps: {project_url}",
+ "flow_title": "{project_url}",
"step": {
"reauth": {
"data": {
diff --git a/homeassistant/components/azure_devops/translations/he.json b/homeassistant/components/azure_devops/translations/he.json
new file mode 100644
index 00000000000..7dc65806162
--- /dev/null
+++ b/homeassistant/components/azure_devops/translations/he.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "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"
+ },
+ "flow_title": "{project_url}"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json
index 460b6132048..f85c6795fd5 100644
--- a/homeassistant/components/azure_devops/translations/hu.json
+++ b/homeassistant/components/azure_devops/translations/hu.json
@@ -8,6 +8,7 @@
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
+ "flow_title": "{project_url}",
"step": {
"reauth": {
"description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait."
diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py
index 0c5ae2b81b8..1c9add1bd8b 100644
--- a/homeassistant/components/azure_event_hub/__init__.py
+++ b/homeassistant/components/azure_event_hub/__init__.py
@@ -23,6 +23,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.json import JSONEncoder
+from homeassistant.helpers.typing import ConfigType
from .const import (
ADDITIONAL_ARGS,
@@ -43,9 +44,9 @@ CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
+ vol.Required(CONF_EVENT_HUB_INSTANCE_NAME): cv.string,
vol.Exclusive(CONF_EVENT_HUB_CON_STRING, "setup_methods"): cv.string,
vol.Exclusive(CONF_EVENT_HUB_NAMESPACE, "setup_methods"): cv.string,
- vol.Optional(CONF_EVENT_HUB_INSTANCE_NAME): cv.string,
vol.Optional(CONF_EVENT_HUB_SAS_POLICY): cv.string,
vol.Optional(CONF_EVENT_HUB_SAS_KEY): cv.string,
vol.Optional(CONF_SEND_INTERVAL, default=5): cv.positive_int,
@@ -61,20 +62,23 @@ CONFIG_SCHEMA = vol.Schema(
)
-async def async_setup(hass, yaml_config):
+async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
"""Activate Azure EH component."""
config = yaml_config[DOMAIN]
if config.get(CONF_EVENT_HUB_CON_STRING):
- client_args = {"conn_str": config[CONF_EVENT_HUB_CON_STRING]}
+ client_args = {
+ "conn_str": config[CONF_EVENT_HUB_CON_STRING],
+ "eventhub_name": config[CONF_EVENT_HUB_INSTANCE_NAME],
+ }
conn_str_client = True
else:
client_args = {
"fully_qualified_namespace": f"{config[CONF_EVENT_HUB_NAMESPACE]}.servicebus.windows.net",
+ "eventhub_name": config[CONF_EVENT_HUB_INSTANCE_NAME],
"credential": EventHubSharedKeyCredential(
policy=config[CONF_EVENT_HUB_SAS_POLICY],
key=config[CONF_EVENT_HUB_SAS_KEY],
),
- "eventhub_name": config[CONF_EVENT_HUB_INSTANCE_NAME],
}
conn_str_client = False
@@ -115,7 +119,7 @@ class AzureEventHub:
self._next_send_remover = None
self.shutdown = False
- async def async_start(self):
+ async def async_start(self) -> None:
"""Start the recorder, suppress logging and register the callbacks and do the first send after five seconds, to capture the startup events."""
# suppress the INFO and below logging on the underlying packages, they are very verbose, even at INFO
logging.getLogger("uamqp").setLevel(logging.WARNING)
@@ -128,7 +132,7 @@ class AzureEventHub:
# schedule the first send after 10 seconds to capture startup events, after that each send will schedule the next after the interval.
self._next_send_remover = async_call_later(self.hass, 10, self.async_send)
- async def async_shutdown(self, _: Event):
+ async def async_shutdown(self, _: Event) -> None:
"""Shut down the AEH by queueing None and calling send."""
if self._next_send_remover:
self._next_send_remover()
@@ -137,14 +141,13 @@ class AzureEventHub:
await self.queue.put((3, (time.monotonic(), None)))
await self.async_send(None)
- async def async_listen(self, event: Event):
+ async def async_listen(self, event: Event) -> None:
"""Listen for new messages on the bus and queue them for AEH."""
await self.queue.put((2, (time.monotonic(), event)))
- async def async_send(self, _):
+ async def async_send(self, _) -> None:
"""Write preprocessed events to eventhub, with retry."""
- client = self._get_client()
- async with client:
+ async with self._get_client() as client:
while not self.queue.empty():
data_batch, dequeue_count = await self.fill_batch(client)
_LOGGER.debug(
@@ -160,14 +163,13 @@ class AzureEventHub:
finally:
for _ in range(dequeue_count):
self.queue.task_done()
- await client.close()
if not self.shutdown:
self._next_send_remover = async_call_later(
self.hass, self._send_interval, self.async_send
)
- async def fill_batch(self, client):
+ async def fill_batch(self, client) -> None:
"""Return a batch of events formatted for writing.
Uses get_nowait instead of await get, because the functions batches and doesn't wait for each single event, the send function is called.
@@ -205,7 +207,7 @@ class AzureEventHub:
return event_batch, dequeue_count
- def _event_to_filtered_event_data(self, event: Event):
+ def _event_to_filtered_event_data(self, event: Event) -> None:
"""Filter event states and create EventData object."""
state = event.data.get("new_state")
if (
@@ -216,7 +218,7 @@ class AzureEventHub:
return None
return EventData(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8"))
- def _get_client(self):
+ def _get_client(self) -> EventHubProducerClient:
"""Get a Event Producer Client."""
if self._conn_str_client:
return EventHubProducerClient.from_connection_string(
diff --git a/homeassistant/components/azure_event_hub/manifest.json b/homeassistant/components/azure_event_hub/manifest.json
index b570f11e28f..9c63af35340 100644
--- a/homeassistant/components/azure_event_hub/manifest.json
+++ b/homeassistant/components/azure_event_hub/manifest.json
@@ -2,7 +2,7 @@
"domain": "azure_event_hub",
"name": "Azure Event Hub",
"documentation": "https://www.home-assistant.io/integrations/azure_event_hub",
- "requirements": ["azure-eventhub==5.1.0"],
+ "requirements": ["azure-eventhub==5.5.0"],
"codeowners": ["@eavanvalkenburg"],
"iot_class": "cloud_push"
}
diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py
index c7e1bac9952..ff97e9af601 100644
--- a/homeassistant/components/binary_sensor/__init__.py
+++ b/homeassistant/components/binary_sensor/__init__.py
@@ -3,19 +3,20 @@ from __future__ import annotations
from datetime import timedelta
import logging
+from typing import Any, final
import voluptuous as vol
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
-from homeassistant.helpers.typing import StateType
-
-# mypy: allow-untyped-defs, no-check-untyped-defs
+from homeassistant.helpers.typing import ConfigType, StateType
_LOGGER = logging.getLogger(__name__)
@@ -126,7 +127,7 @@ DEVICE_CLASSES = [
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Track states and offer events for binary sensors."""
component = hass.data[DOMAIN] = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL
@@ -136,26 +137,30 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class BinarySensorEntity(Entity):
"""Represent a binary sensor."""
_attr_is_on: bool | None = None
+ _attr_state: None = None
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
return self._attr_is_on
+ @final
@property
def state(self) -> StateType:
"""Return the state of the binary sensor."""
@@ -165,9 +170,9 @@ class BinarySensorEntity(Entity):
class BinarySensorDevice(BinarySensorEntity):
"""Represent a binary sensor (for backwards compatibility)."""
- def __init_subclass__(cls, **kwargs):
+ def __init_subclass__(cls, **kwargs: Any):
"""Print deprecation warning."""
- super().__init_subclass__(**kwargs)
+ super().__init_subclass__(**kwargs) # type: ignore[call-arg]
_LOGGER.warning(
"BinarySensorDevice is deprecated, modify %s to extend BinarySensorEntity",
cls.__name__,
diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py
index 8c506634200..eed5c3f5896 100644
--- a/homeassistant/components/binary_sensor/device_condition.py
+++ b/homeassistant/components/binary_sensor/device_condition.py
@@ -4,9 +4,10 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON
-from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE
+from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_TYPE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import condition, config_validation as cv
+from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.entity_registry import (
async_entries_for_device,
async_get_registry,
@@ -216,10 +217,7 @@ async def async_get_conditions(
]
for entry in entries:
- device_class = DEVICE_CLASS_NONE
- state = hass.states.get(entry.entity_id)
- if state and ATTR_DEVICE_CLASS in state.attributes:
- device_class = state.attributes[ATTR_DEVICE_CLASS]
+ device_class = get_device_class(hass, entry.entity_id) or DEVICE_CLASS_NONE
templates = ENTITY_CONDITIONS.get(
device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE]
diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py
index b87a761a7a1..ad5c26ed04f 100644
--- a/homeassistant/components/binary_sensor/device_trigger.py
+++ b/homeassistant/components/binary_sensor/device_trigger.py
@@ -1,14 +1,15 @@
"""Provides device triggers for binary sensors."""
import voluptuous as vol
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.device_automation.const import (
CONF_TURNED_OFF,
CONF_TURNED_ON,
)
from homeassistant.components.homeassistant.triggers import state as state_trigger
-from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE
+from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_TYPE
from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.entity_registry import async_entries_for_device
from . import (
@@ -177,7 +178,7 @@ ENTITY_TRIGGERS = {
}
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON),
@@ -220,10 +221,7 @@ async def async_get_triggers(hass, device_id):
]
for entry in entries:
- device_class = DEVICE_CLASS_NONE
- state = hass.states.get(entry.entity_id)
- if state:
- device_class = state.attributes.get(ATTR_DEVICE_CLASS)
+ device_class = get_device_class(hass, entry.entity_id) or DEVICE_CLASS_NONE
templates = ENTITY_TRIGGERS.get(
device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE]
diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json
index 5f4fb949b34..b7375f3175b 100644
--- a/homeassistant/components/binary_sensor/translations/he.json
+++ b/homeassistant/components/binary_sensor/translations/he.json
@@ -2,10 +2,14 @@
"device_automation": {
"condition_type": {
"is_cold": "{entity_name} \u05e7\u05e8",
+ "is_light": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05d0\u05d5\u05e8",
+ "is_no_light": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05d0\u05d5\u05e8",
"is_not_cold": "{entity_name} \u05dc\u05d0 \u05e7\u05e8"
},
"trigger_type": {
"cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05e7\u05e8",
+ "light": "{entity_name} \u05d4\u05ea\u05d7\u05d9\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05d0\u05d5\u05e8",
+ "no_light": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05d0\u05d5\u05e8",
"not_cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05dc\u05d0 \u05e7\u05e8"
}
},
@@ -18,6 +22,10 @@
"off": "\u05e0\u05d5\u05e8\u05de\u05dc\u05d9",
"on": "\u05e0\u05de\u05d5\u05da"
},
+ "battery_charging": {
+ "off": "\u05dc\u05d0 \u05e0\u05d8\u05e2\u05df",
+ "on": "\u05e0\u05d8\u05e2\u05df"
+ },
"cold": {
"off": "\u05e8\u05d2\u05d9\u05dc",
"on": "\u05e7\u05b7\u05e8"
@@ -42,6 +50,10 @@
"off": "\u05e8\u05d2\u05d9\u05dc",
"on": "\u05d7\u05dd"
},
+ "light": {
+ "off": "\u05d0\u05d9\u05df \u05d0\u05d5\u05e8",
+ "on": "\u05d6\u05d5\u05d4\u05d4 \u05d0\u05d5\u05e8"
+ },
"lock": {
"off": "\u05e0\u05e2\u05d5\u05dc",
"on": "\u05dc\u05d0 \u05e0\u05e2\u05d5\u05dc"
@@ -54,6 +66,10 @@
"off": "\u05e0\u05e7\u05d9",
"on": "\u05d6\u05d5\u05d4\u05d4"
},
+ "moving": {
+ "off": "\u05dc\u05d0 \u05d6\u05d6",
+ "on": "\u05e0\u05e2"
+ },
"occupancy": {
"off": "\u05e0\u05e7\u05d9",
"on": "\u05d6\u05d5\u05d4\u05d4"
@@ -62,6 +78,9 @@
"off": "\u05e1\u05d2\u05d5\u05e8",
"on": "\u05e4\u05ea\u05d5\u05d7"
},
+ "plug": {
+ "off": "\u05de\u05e0\u05d5\u05ea\u05e7"
+ },
"presence": {
"off": "\u05dc\u05d0 \u05e0\u05d5\u05db\u05d7",
"on": "\u05e0\u05d5\u05db\u05d7"
diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py
index fe2265ed78d..33d09f460db 100644
--- a/homeassistant/components/blebox/__init__.py
+++ b/homeassistant/components/blebox/__init__.py
@@ -21,7 +21,7 @@ PLATFORMS = ["cover", "sensor", "switch", "air_quality", "light", "climate"]
PARALLEL_UPDATES = 0
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up BleBox devices from a config entry."""
websession = async_get_clientsession(hass)
diff --git a/homeassistant/components/blebox/translations/de.json b/homeassistant/components/blebox/translations/de.json
index 37c8dde54e5..508e4b66ee6 100644
--- a/homeassistant/components/blebox/translations/de.json
+++ b/homeassistant/components/blebox/translations/de.json
@@ -9,7 +9,7 @@
"unknown": "Unerwarteter Fehler",
"unsupported_version": "Das BleBox-Ger\u00e4t hat eine veraltete Firmware. Bitte aktualisieren Sie es zuerst."
},
- "flow_title": "BleBox-Ger\u00e4t: {name} ( {host} )",
+ "flow_title": "{name} ( {host} )",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/blebox/translations/he.json b/homeassistant/components/blebox/translations/he.json
index 001f8457f14..c904fe2bb04 100644
--- a/homeassistant/components/blebox/translations/he.json
+++ b/homeassistant/components/blebox/translations/he.json
@@ -1,5 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/blebox/translations/hu.json b/homeassistant/components/blebox/translations/hu.json
index 9649d70d976..97a6c1bdc18 100644
--- a/homeassistant/components/blebox/translations/hu.json
+++ b/homeassistant/components/blebox/translations/hu.json
@@ -8,7 +8,7 @@
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt",
"unsupported_version": "A BleBox eszk\u00f6z elavult firmware-rel rendelkezik. El\u0151sz\u00f6r friss\u00edtsd."
},
- "flow_title": "BleBox eszk\u00f6z: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/blebox/translations/pl.json b/homeassistant/components/blebox/translations/pl.json
index 21540182505..0174380794e 100644
--- a/homeassistant/components/blebox/translations/pl.json
+++ b/homeassistant/components/blebox/translations/pl.json
@@ -16,7 +16,7 @@
"host": "Adres IP",
"port": "Port"
},
- "description": "Skonfiguruj BleBox, aby zintegrowa\u0107 si\u0119 z Home Assistantem.",
+ "description": "Skonfiguruj BleBox, aby zintegrowa\u0107 go z Home Assistantem.",
"title": "Konfiguracja urz\u0105dzenia BleBox"
}
}
diff --git a/homeassistant/components/blink/translations/de.json b/homeassistant/components/blink/translations/de.json
index 86fa2b609ad..8d3911d5f80 100644
--- a/homeassistant/components/blink/translations/de.json
+++ b/homeassistant/components/blink/translations/de.json
@@ -14,7 +14,7 @@
"data": {
"2fa": "Zwei-Faktor Authentifizierungscode"
},
- "description": "Gib die an deine E-Mail gesendete Pin ein. Wenn die E-Mail keine PIN enth\u00e4lt, lass das Feld leer.",
+ "description": "Gib die an deine E-Mail gesendete Pin ein",
"title": "Zwei-Faktor-Authentifizierung"
},
"user": {
diff --git a/homeassistant/components/blink/translations/he.json b/homeassistant/components/blink/translations/he.json
new file mode 100644
index 00000000000..764c41136e2
--- /dev/null
+++ b/homeassistant/components/blink/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "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",
+ "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "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": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bluetooth_tracker/manifest.json b/homeassistant/components/bluetooth_tracker/manifest.json
index a41720c2c4f..ccf48a9b8c3 100644
--- a/homeassistant/components/bluetooth_tracker/manifest.json
+++ b/homeassistant/components/bluetooth_tracker/manifest.json
@@ -2,7 +2,7 @@
"domain": "bluetooth_tracker",
"name": "Bluetooth Tracker",
"documentation": "https://www.home-assistant.io/integrations/bluetooth_tracker",
- "requirements": ["bt_proximity==0.2", "pybluez==0.22"],
+ "requirements": ["bt_proximity==0.2.1", "pybluez==0.22"],
"codeowners": [],
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py
index 79461082acd..599892d6a03 100644
--- a/homeassistant/components/bmw_connected_drive/__init__.py
+++ b/homeassistant/components/bmw_connected_drive/__init__.py
@@ -11,6 +11,7 @@ from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
+ CONF_DEVICE_ID,
CONF_NAME,
CONF_PASSWORD,
CONF_REGION,
@@ -18,7 +19,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers import discovery
+from homeassistant.helpers import device_registry, discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo, Entity
from homeassistant.helpers.event import track_utc_time_change
@@ -51,7 +52,12 @@ ACCOUNT_SCHEMA = vol.Schema(
CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: ACCOUNT_SCHEMA}}, extra=vol.ALLOW_EXTRA)
-SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string})
+SERVICE_SCHEMA = vol.Schema(
+ vol.Any(
+ {vol.Required(ATTR_VIN): cv.string},
+ {vol.Required(CONF_DEVICE_ID): cv.string},
+ )
+)
DEFAULT_OPTIONS = {
CONF_READ_ONLY: False,
@@ -101,7 +107,7 @@ def _async_migrate_options_from_data_if_missing(hass, entry):
hass.config_entries.async_update_entry(entry, data=data, options=options)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up BMW Connected Drive from a config entry."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(DATA_ENTRIES, {})
@@ -207,8 +213,15 @@ def setup_account(entry: ConfigEntry, hass, name: str) -> BMWConnectedDriveAccou
def execute_service(call):
"""Execute a service for a vehicle."""
- vin = call.data[ATTR_VIN]
+ vin = call.data.get(ATTR_VIN)
+ device_id = call.data.get(CONF_DEVICE_ID)
+
vehicle = None
+
+ if not vin and device_id:
+ device = device_registry.async_get(hass).async_get(device_id)
+ vin = next(iter(device.identifiers))[1]
+
# Double check for read_only accounts as another account could create the services
for entry_data in [
e
diff --git a/homeassistant/components/bmw_connected_drive/services.yaml b/homeassistant/components/bmw_connected_drive/services.yaml
index 563e14e5577..964fb8ab39b 100644
--- a/homeassistant/components/bmw_connected_drive/services.yaml
+++ b/homeassistant/components/bmw_connected_drive/services.yaml
@@ -6,14 +6,20 @@
light_flash:
name: Flash lights
description: >
- Flash the lights of the vehicle. The vehicle is identified via the vin
- (see below).
+ Flash the lights of the vehicle. The vehicle is identified either via its
+ device entry or the VIN. If a VIN is specified, the device entry will be ignored.
fields:
+ device_id:
+ name: Car
+ description: The BMW Connected Drive device
+ selector:
+ device:
+ integration: bmw_connected_drive
vin:
name: VIN
- description: >
- The vehicle identification number (VIN) of the vehicle, 17 characters
- required: true
+ description: The vehicle identification number (VIN) of the vehicle, 17 characters
+ advanced: true
+ required: false
example: WBANXXXXXX1234567
selector:
text:
@@ -21,14 +27,20 @@ light_flash:
sound_horn:
name: Sound horn
description: >
- Sound the horn of the vehicle. The vehicle is identified via the vin
- (see below).
+ Sound the horn of the vehicle. The vehicle is identified either via its
+ device entry or the VIN. If a VIN is specified, the device entry will be ignored.
fields:
+ device_id:
+ name: Car
+ description: The BMW Connected Drive device
+ selector:
+ device:
+ integration: bmw_connected_drive
vin:
name: VIN
- description: >
- The vehicle identification number (VIN) of the vehicle, 17 characters
- required: true
+ description: The vehicle identification number (VIN) of the vehicle, 17 characters
+ advanced: true
+ required: false
example: WBANXXXXXX1234567
selector:
text:
@@ -38,14 +50,20 @@ activate_air_conditioning:
description: >
Start the air conditioning of the vehicle. What exactly is started here
depends on the type of vehicle. It might range from just ventilation over
- auxiliary heating to real air conditioning. The vehicle is identified via
- the vin (see below).
+ auxiliary heating to real air conditioning. The vehicle is identified either via its
+ device entry or the VIN. If a VIN is specified, the device entry will be ignored.
fields:
+ device_id:
+ name: Car
+ description: The BMW Connected Drive device
+ selector:
+ device:
+ integration: bmw_connected_drive
vin:
name: VIN
- description: >
- The vehicle identification number (VIN) of the vehicle, 17 characters
- required: true
+ description: The vehicle identification number (VIN) of the vehicle, 17 characters
+ advanced: true
+ required: false
example: WBANXXXXXX1234567
selector:
text:
@@ -53,14 +71,20 @@ activate_air_conditioning:
find_vehicle:
name: Find vehicle
description: >
- Request vehicle to update the gps location. The vehicle is identified via the vin
- (see below).
+ Request vehicle to update the GPS location. The vehicle is identified either via its
+ device entry or the VIN. If a VIN is specified, the device entry will be ignored.
fields:
+ device_id:
+ name: Car
+ description: The BMW Connected Drive device
+ selector:
+ device:
+ integration: bmw_connected_drive
vin:
name: VIN
- description: >
- The vehicle identification number (VIN) of the vehicle, 17 characters
- required: true
+ description: The vehicle identification number (VIN) of the vehicle, 17 characters
+ advanced: true
+ required: false
example: WBANXXXXXX1234567
selector:
text:
diff --git a/homeassistant/components/bmw_connected_drive/translations/de.json b/homeassistant/components/bmw_connected_drive/translations/de.json
index d274719d7d0..85e27b5b3e4 100644
--- a/homeassistant/components/bmw_connected_drive/translations/de.json
+++ b/homeassistant/components/bmw_connected_drive/translations/de.json
@@ -16,5 +16,15 @@
}
}
}
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "Schreibgesch\u00fctzt (nur Sensoren und Notify, keine Ausf\u00fchrung von Diensten, kein Abschlie\u00dfen)",
+ "use_location": "Standort des Home Assistant f\u00fcr die Abfrage des Fahrzeugstandorts verwenden (erforderlich f\u00fcr nicht i3/i8 Fahrzeuge, die vor 7/2014 produziert wurden)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/he.json b/homeassistant/components/bmw_connected_drive/translations/he.json
new file mode 100644
index 00000000000..49f37a267d0
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/he.json
@@ -0,0 +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"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bond/translations/de.json b/homeassistant/components/bond/translations/de.json
index 4b7372a4526..934e166e0d5 100644
--- a/homeassistant/components/bond/translations/de.json
+++ b/homeassistant/components/bond/translations/de.json
@@ -9,7 +9,7 @@
"old_firmware": "Nicht unterst\u00fctzte alte Firmware auf dem Bond-Ger\u00e4t - bitte aktualisiere, bevor du fortf\u00e4hrst",
"unknown": "Unerwarteter Fehler"
},
- "flow_title": "Bond: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"confirm": {
"data": {
diff --git a/homeassistant/components/bond/translations/he.json b/homeassistant/components/bond/translations/he.json
new file mode 100644
index 00000000000..1cbd27129b2
--- /dev/null
+++ b/homeassistant/components/bond/translations/he.json
@@ -0,0 +1,26 @@
+{
+ "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",
+ "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} ({host})",
+ "step": {
+ "confirm": {
+ "data": {
+ "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4"
+ }
+ },
+ "user": {
+ "data": {
+ "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4",
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json
index 868ef455f5d..535d3586b93 100644
--- a/homeassistant/components/bond/translations/hu.json
+++ b/homeassistant/components/bond/translations/hu.json
@@ -9,7 +9,7 @@
"old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rj\u00fck friss\u00edtsd, miel\u0151tt folytatn\u00e1d",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "Bond: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"confirm": {
"data": {
diff --git a/homeassistant/components/bosch_shc/__init__.py b/homeassistant/components/bosch_shc/__init__.py
index a315405365c..f68a2b68467 100644
--- a/homeassistant/components/bosch_shc/__init__.py
+++ b/homeassistant/components/bosch_shc/__init__.py
@@ -19,9 +19,7 @@ from .const import (
DOMAIN,
)
-PLATFORMS = [
- "binary_sensor",
-]
+PLATFORMS = ["binary_sensor", "sensor"]
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/bosch_shc/binary_sensor.py b/homeassistant/components/bosch_shc/binary_sensor.py
index ef2d35097e1..d2c2df838c5 100644
--- a/homeassistant/components/bosch_shc/binary_sensor.py
+++ b/homeassistant/components/bosch_shc/binary_sensor.py
@@ -1,7 +1,8 @@
"""Platform for binarysensor integration."""
-from boschshcpy import SHCSession, SHCShutterContact
+from boschshcpy import SHCBatteryDevice, SHCSession, SHCShutterContact
from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_BATTERY,
DEVICE_CLASS_DOOR,
DEVICE_CLASS_WINDOW,
BinarySensorEntity,
@@ -25,6 +26,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
)
+ for binary_sensor in (
+ session.device_helper.motion_detectors
+ + session.device_helper.shutter_contacts
+ + session.device_helper.smoke_detectors
+ + session.device_helper.thermostats
+ + session.device_helper.twinguards
+ + session.device_helper.universal_switches
+ + session.device_helper.wallthermostats
+ + session.device_helper.water_leakage_detectors
+ ):
+ if binary_sensor.supports_batterylevel:
+ entities.append(
+ BatterySensor(
+ device=binary_sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+
if entities:
async_add_entities(entities)
@@ -47,3 +67,29 @@ class ShutterContactSensor(SHCEntity, BinarySensorEntity):
"GENERIC": DEVICE_CLASS_WINDOW,
}
return switcher.get(self._device.device_class, DEVICE_CLASS_WINDOW)
+
+
+class BatterySensor(SHCEntity, BinarySensorEntity):
+ """Representation of a SHC battery reporting sensor."""
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._device.serial}_battery"
+
+ @property
+ def name(self):
+ """Return the name of this sensor."""
+ return f"{self._device.name} Battery"
+
+ @property
+ def is_on(self):
+ """Return the state of the sensor."""
+ return (
+ self._device.batterylevel != SHCBatteryDevice.BatteryLevelService.State.OK
+ )
+
+ @property
+ def device_class(self):
+ """Return the class of the sensor."""
+ return DEVICE_CLASS_BATTERY
diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json
index 7922450ccde..d4a8498fba3 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.17"],
+ "requirements": ["boschshcpy==0.2.19"],
"zeroconf": [
{"type": "_http._tcp.local.", "name": "bosch shc*"}
],
diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py
new file mode 100644
index 00000000000..9f3cf2d5bc3
--- /dev/null
+++ b/homeassistant/components/bosch_shc/sensor.py
@@ -0,0 +1,406 @@
+"""Platform for sensor integration."""
+from boschshcpy import SHCSession
+
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import (
+ CONCENTRATION_PARTS_PER_MILLION,
+ DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_POWER,
+ DEVICE_CLASS_TEMPERATURE,
+ ENERGY_KILO_WATT_HOUR,
+ PERCENTAGE,
+ POWER_WATT,
+ TEMP_CELSIUS,
+)
+
+from .const import DATA_SESSION, DOMAIN
+from .entity import SHCEntity
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the SHC sensor platform."""
+ entities = []
+ session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION]
+
+ for sensor in session.device_helper.thermostats:
+ entities.append(
+ TemperatureSensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+ entities.append(
+ ValveTappetSensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+
+ for sensor in session.device_helper.wallthermostats:
+ entities.append(
+ TemperatureSensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+ entities.append(
+ HumiditySensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+
+ for sensor in session.device_helper.twinguards:
+ entities.append(
+ TemperatureSensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+ entities.append(
+ HumiditySensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+ entities.append(
+ PuritySensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+ entities.append(
+ AirQualitySensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+ entities.append(
+ TemperatureRatingSensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+ entities.append(
+ HumidityRatingSensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+ entities.append(
+ PurityRatingSensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+
+ for sensor in session.device_helper.smart_plugs:
+ entities.append(
+ PowerSensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+ entities.append(
+ EnergySensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+
+ for sensor in session.device_helper.smart_plugs_compact:
+ entities.append(
+ PowerSensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+ entities.append(
+ EnergySensor(
+ device=sensor,
+ parent_id=session.information.unique_id,
+ entry_id=config_entry.entry_id,
+ )
+ )
+
+ if entities:
+ async_add_entities(entities)
+
+
+class TemperatureSensor(SHCEntity, SensorEntity):
+ """Representation of a SHC temperature reporting sensor."""
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._device.serial}_temperature"
+
+ @property
+ def name(self):
+ """Return the name of this sensor."""
+ return f"{self._device.name} Temperature"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._device.temperature
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of the sensor."""
+ return TEMP_CELSIUS
+
+ @property
+ def device_class(self):
+ """Return the class of this device."""
+ return DEVICE_CLASS_TEMPERATURE
+
+
+class HumiditySensor(SHCEntity, SensorEntity):
+ """Representation of a SHC humidity reporting sensor."""
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._device.serial}_humidity"
+
+ @property
+ def name(self):
+ """Return the name of this sensor."""
+ return f"{self._device.name} Humidity"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._device.humidity
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of the sensor."""
+ return PERCENTAGE
+
+ @property
+ def device_class(self):
+ """Return the class of this device."""
+ return DEVICE_CLASS_HUMIDITY
+
+
+class PuritySensor(SHCEntity, SensorEntity):
+ """Representation of a SHC purity reporting sensor."""
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._device.serial}_purity"
+
+ @property
+ def name(self):
+ """Return the name of this sensor."""
+ return f"{self._device.name} Purity"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._device.purity
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of the sensor."""
+ return CONCENTRATION_PARTS_PER_MILLION
+
+ @property
+ def icon(self):
+ """Return the icon of the sensor."""
+ return "mdi:molecule-co2"
+
+
+class AirQualitySensor(SHCEntity, SensorEntity):
+ """Representation of a SHC airquality reporting sensor."""
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._device.serial}_airquality"
+
+ @property
+ def name(self):
+ """Return the name of this sensor."""
+ return f"{self._device.name} Air Quality"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._device.combined_rating.name
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ return {
+ "rating_description": self._device.description,
+ }
+
+
+class TemperatureRatingSensor(SHCEntity, SensorEntity):
+ """Representation of a SHC temperature rating sensor."""
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._device.serial}_temperature_rating"
+
+ @property
+ def name(self):
+ """Return the name of this sensor."""
+ return f"{self._device.name} Temperature Rating"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._device.temperature_rating.name
+
+
+class HumidityRatingSensor(SHCEntity, SensorEntity):
+ """Representation of a SHC humidity rating sensor."""
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._device.serial}_humidity_rating"
+
+ @property
+ def name(self):
+ """Return the name of this sensor."""
+ return f"{self._device.name} Humidity Rating"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._device.humidity_rating.name
+
+
+class PurityRatingSensor(SHCEntity, SensorEntity):
+ """Representation of a SHC purity rating sensor."""
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._device.serial}_purity_rating"
+
+ @property
+ def name(self):
+ """Return the name of this sensor."""
+ return f"{self._device.name} Purity Rating"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._device.purity_rating.name
+
+
+class PowerSensor(SHCEntity, SensorEntity):
+ """Representation of a SHC power reporting sensor."""
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._device.serial}_power"
+
+ @property
+ def name(self):
+ """Return the name of this sensor."""
+ return f"{self._device.name} Power"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._device.powerconsumption
+
+ @property
+ def device_class(self):
+ """Return the class of this device."""
+ return DEVICE_CLASS_POWER
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of the sensor."""
+ return POWER_WATT
+
+
+class EnergySensor(SHCEntity, SensorEntity):
+ """Representation of a SHC energy reporting sensor."""
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._device.serial}_energy"
+
+ @property
+ def name(self):
+ """Return the name of this sensor."""
+ return f"{self._device.name} Energy"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._device.energyconsumption / 1000.0
+
+ @property
+ def device_class(self):
+ """Return the class of this device."""
+ return DEVICE_CLASS_ENERGY
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of the sensor."""
+ return ENERGY_KILO_WATT_HOUR
+
+
+class ValveTappetSensor(SHCEntity, SensorEntity):
+ """Representation of a SHC valve tappet reporting sensor."""
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._device.serial}_valvetappet"
+
+ @property
+ def name(self):
+ """Return the name of this sensor."""
+ return f"{self._device.name} Valvetappet"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._device.position
+
+ @property
+ def icon(self):
+ """Return the icon of the sensor."""
+ return "mdi:gauge"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of the sensor."""
+ return PERCENTAGE
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ return {
+ "valve_tappet_state": self._device.valvestate.name,
+ }
diff --git a/homeassistant/components/bosch_shc/translations/de.json b/homeassistant/components/bosch_shc/translations/de.json
index 8d3e47d0ab6..110e986e106 100644
--- a/homeassistant/components/bosch_shc/translations/de.json
+++ b/homeassistant/components/bosch_shc/translations/de.json
@@ -7,17 +7,32 @@
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "pairing_failed": "Pairing fehlgeschlagen; bitte pr\u00fcfen Sie, ob sich der Bosch Smart Home Controller im Pairing-Modus befindet (LED blinkt) und ob Ihr Passwort korrekt ist.",
+ "session_error": "Sitzungsfehler: API gab Non-OK-Ergebnis zur\u00fcck.",
"unknown": "Unerwarteter Fehler"
},
+ "flow_title": "Bosch SHC: {name}",
"step": {
+ "confirm_discovery": {
+ "description": "Bitte dr\u00fccken Sie die frontseitige Taste des Bosch Smart Home Controllers, bis die LED zu blinken beginnt.\nSind Sie bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?"
+ },
+ "credentials": {
+ "data": {
+ "password": "Passwort des Smart Home Controllers"
+ }
+ },
"reauth_confirm": {
+ "description": "Die bosch_shc-Integration muss Ihr Konto neu authentifizieren",
"title": "Integration erneut authentifizieren"
},
"user": {
"data": {
"host": "Host"
- }
+ },
+ "description": "Richten Sie Ihren Bosch Smart Home Controller ein, um die \u00dcberwachung und Steuerung mit Home Assistant zu erm\u00f6glichen.",
+ "title": "SHC Authentifizierungsparameter"
}
}
- }
+ },
+ "title": "Bosch SHC"
}
\ No newline at end of file
diff --git a/homeassistant/components/bosch_shc/translations/es.json b/homeassistant/components/bosch_shc/translations/es.json
new file mode 100644
index 00000000000..2b3d4ca7479
--- /dev/null
+++ b/homeassistant/components/bosch_shc/translations/es.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "error": {
+ "pairing_failed": "El emparejamiento ha fallado; compruebe que el Bosch Smart Home Controller est\u00e1 en modo de emparejamiento (el LED parpadea) y que su contrase\u00f1a es correcta.",
+ "session_error": "Error de sesi\u00f3n: La API devuelve un resultado no correcto."
+ },
+ "flow_title": "Bosch SHC: {name}",
+ "step": {
+ "confirm_discovery": {
+ "description": "Pulse el bot\u00f3n frontal del Smart Home Controller de Bosch hasta que el LED empiece a parpadear.\n\u00bfPreparado para seguir configurando {model} @ {host} con Home Assistant?"
+ },
+ "credentials": {
+ "data": {
+ "password": "Contrase\u00f1a del controlador smart home"
+ }
+ },
+ "reauth_confirm": {
+ "description": "La integraci\u00f3n bosch_shc necesita volver a autentificar su cuenta"
+ },
+ "user": {
+ "description": "Configura tu Bosch Smart Home Controller para permitir la supervisi\u00f3n y el control con Home Assistant.",
+ "title": "Par\u00e1metros de autenticaci\u00f3n SHC"
+ }
+ }
+ },
+ "title": "Bosch SHC"
+}
\ No newline at end of file
diff --git a/homeassistant/components/bosch_shc/translations/he.json b/homeassistant/components/bosch_shc/translations/he.json
new file mode 100644
index 00000000000..f7b240ce079
--- /dev/null
+++ b/homeassistant/components/bosch_shc/translations/he.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "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",
+ "pairing_failed": "\u05d4\u05e9\u05d9\u05d5\u05da \u05e0\u05db\u05e9\u05dc; \u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05e9\u05d1\u05e7\u05e8 \u05d4\u05d1\u05d9\u05ea \u05d4\u05d7\u05db\u05dd \u05e9\u05dc Bosch \u05e0\u05de\u05e6\u05d0 \u05d1\u05de\u05e6\u05d1 \u05e9\u05d9\u05d5\u05da (\u05de\u05d4\u05d1\u05d4\u05d1 LED) \u05db\u05de\u05d5 \u05d2\u05dd \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05e0\u05db\u05d5\u05e0\u05d4.",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "confirm_discovery": {
+ "description": "\u05dc\u05d7\u05e5 \u05e2\u05dc \u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05e6\u05d3 \u05d4\u05e7\u05d3\u05de\u05d9 \u05e9\u05dc \u05d1\u05e7\u05e8\u05ea \u05d4\u05d1\u05d9\u05ea \u05d4\u05d7\u05db\u05dd \u05e9\u05dc Bosch \u05e2\u05d3 \u05e9\u05d4\u05e0\u05d5\u05e8\u05d9\u05ea \u05ea\u05ea\u05d7\u05d9\u05dc \u05dc\u05d4\u05d1\u05d4\u05d1.\n \u05de\u05d5\u05db\u05df \u05dc\u05d4\u05de\u05e9\u05d9\u05da \u05d5\u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} @ {host} \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea Home Assistant?"
+ },
+ "credentials": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05ea \u05d1\u05e7\u05e8 \u05d4\u05d1\u05d9\u05ea \u05d4\u05d7\u05db\u05dd"
+ }
+ },
+ "reauth_confirm": {
+ "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1"
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bosch_shc/translations/hu.json b/homeassistant/components/bosch_shc/translations/hu.json
new file mode 100644
index 00000000000..9cd2a0be6c1
--- /dev/null
+++ b/homeassistant/components/bosch_shc/translations/hu.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "flow_title": "Bosch SHC: {name}",
+ "step": {
+ "reauth_confirm": {
+ "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se"
+ },
+ "user": {
+ "data": {
+ "host": "Hoszt"
+ }
+ }
+ }
+ },
+ "title": "Bosch SHC"
+}
\ No newline at end of file
diff --git a/homeassistant/components/bosch_shc/translations/pl.json b/homeassistant/components/bosch_shc/translations/pl.json
new file mode 100644
index 00000000000..c140bf6b6f8
--- /dev/null
+++ b/homeassistant/components/bosch_shc/translations/pl.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie 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",
+ "pairing_failed": "Parowanie nie powiod\u0142o si\u0119. Sprawd\u017a, czy kontroler Bosch Smart Home jest w trybie parowania (miga dioda LED) i czy Twoje has\u0142o jest prawid\u0142owe.",
+ "session_error": "B\u0142\u0105d sesji: API zwr\u00f3ci\u0142o niepoprawny wynik.",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "flow_title": "Bosch SHC: {name}",
+ "step": {
+ "confirm_discovery": {
+ "description": "Naci\u015bnij przycisk z przodu kontrolera Bosch Smart Home, a\u017c dioda LED zacznie miga\u0107. Chcesz kontynuowa\u0107 konfiguracj\u0119 {model} @ {host} z Home Assistantem?"
+ },
+ "credentials": {
+ "data": {
+ "password": "Has\u0142o kontrolera"
+ }
+ },
+ "reauth_confirm": {
+ "description": "Integracja Bosch_SHC wymaga ponownego uwierzytelnienia Twojego konta",
+ "title": "Ponownie uwierzytelnij integracj\u0119"
+ },
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP"
+ },
+ "description": "Skonfiguruj kontroler Bosch Smart Home, aby umo\u017cliwi\u0107 monitorowanie i sterowanie za pomoc\u0105 Home Assistanta.",
+ "title": "Parametry uwierzytelniania SHC"
+ }
+ }
+ },
+ "title": "Bosch SHC"
+}
\ No newline at end of file
diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py
index 0097964e298..eecf8533800 100644
--- a/homeassistant/components/braviatv/__init__.py
+++ b/homeassistant/components/braviatv/__init__.py
@@ -1,26 +1,39 @@
"""The Bravia TV component."""
+import asyncio
+from datetime import timedelta
+import logging
from bravia_tv import BraviaRC
+from bravia_tv.braviarc import NoIPControl
-from homeassistant.const import CONF_HOST, CONF_MAC
+from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
+from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
+from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN
+from homeassistant.helpers.debounce import Debouncer
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
-from .const import BRAVIARC, DOMAIN, UNDO_UPDATE_LISTENER
+from .const import CLIENTID_PREFIX, CONF_IGNORED_SOURCES, DOMAIN, NICKNAME
-PLATFORMS = ["media_player"]
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN]
+SCAN_INTERVAL = timedelta(seconds=10)
async def async_setup_entry(hass, config_entry):
"""Set up a config entry."""
host = config_entry.data[CONF_HOST]
mac = config_entry.data[CONF_MAC]
+ pin = config_entry.data[CONF_PIN]
+ ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, [])
- undo_listener = config_entry.add_update_listener(update_listener)
+ coordinator = BraviaTVCoordinator(hass, host, mac, pin, ignored_sources)
+ config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
+
+ await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
- hass.data[DOMAIN][config_entry.entry_id] = {
- BRAVIARC: BraviaRC(host, mac),
- UNDO_UPDATE_LISTENER: undo_listener,
- }
+ hass.data[DOMAIN][config_entry.entry_id] = coordinator
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
@@ -33,8 +46,6 @@ async def async_unload_entry(hass, config_entry):
config_entry, PLATFORMS
)
- hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
-
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
@@ -44,3 +55,225 @@ async def async_unload_entry(hass, config_entry):
async def update_listener(hass, config_entry):
"""Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id)
+
+
+class BraviaTVCoordinator(DataUpdateCoordinator[None]):
+ """Representation of a Bravia TV Coordinator.
+
+ An instance is used per device to share the same power state between
+ several platforms.
+ """
+
+ def __init__(self, hass, host, mac, pin, ignored_sources):
+ """Initialize Bravia TV Client."""
+
+ self.braviarc = BraviaRC(host, mac)
+ self.pin = pin
+ self.ignored_sources = ignored_sources
+ self.muted = False
+ self.channel_name = None
+ self.channel_number = None
+ self.media_title = None
+ self.source = None
+ self.source_list = []
+ self.original_content_list = []
+ self.content_mapping = {}
+ self.duration = None
+ self.content_uri = None
+ self.start_date_time = None
+ self.program_media_type = None
+ self.audio_output = None
+ self.min_volume = None
+ self.max_volume = None
+ self.volume_level = None
+ self.is_on = False
+ # Assume that the TV is in Play mode
+ self.playing = True
+ self.state_lock = asyncio.Lock()
+
+ super().__init__(
+ hass,
+ _LOGGER,
+ name=DOMAIN,
+ update_interval=SCAN_INTERVAL,
+ request_refresh_debouncer=Debouncer(
+ hass, _LOGGER, cooldown=1.0, immediate=False
+ ),
+ )
+
+ def _send_command(self, command, repeats=1):
+ """Send a command to the TV."""
+ for _ in range(repeats):
+ for cmd in command:
+ self.braviarc.send_command(cmd)
+
+ def _get_source(self):
+ """Return the name of the source."""
+ for key, value in self.content_mapping.items():
+ if value == self.content_uri:
+ return key
+
+ def _refresh_volume(self):
+ """Refresh volume information."""
+ volume_info = self.braviarc.get_volume_info(self.audio_output)
+ if volume_info is not None:
+ volume = volume_info.get("volume")
+ self.volume_level = volume / 100 if volume is not None else None
+ self.audio_output = volume_info.get("target")
+ self.min_volume = volume_info.get("minVolume")
+ self.max_volume = volume_info.get("maxVolume")
+ self.muted = volume_info.get("mute")
+ return True
+ return False
+
+ def _refresh_channels(self):
+ """Refresh source and channels list."""
+ if not self.source_list:
+ self.content_mapping = self.braviarc.load_source_list()
+ self.source_list = []
+ if not self.content_mapping:
+ return False
+ for key in self.content_mapping:
+ if key not in self.ignored_sources:
+ self.source_list.append(key)
+ return True
+
+ def _refresh_playing_info(self):
+ """Refresh playing information."""
+ playing_info = self.braviarc.get_playing_info()
+ program_name = playing_info.get("programTitle")
+ self.channel_name = playing_info.get("title")
+ self.program_media_type = playing_info.get("programMediaType")
+ self.channel_number = playing_info.get("dispNum")
+ self.content_uri = playing_info.get("uri")
+ self.source = self._get_source()
+ self.duration = playing_info.get("durationSec")
+ self.start_date_time = playing_info.get("startDateTime")
+ if not playing_info:
+ self.channel_name = "App"
+ if self.channel_name is not None:
+ self.media_title = self.channel_name
+ if program_name is not None:
+ self.media_title = f"{self.media_title}: {program_name}"
+ else:
+ self.media_title = None
+
+ def _update_tv_data(self):
+ """Connect and update TV info."""
+ power_status = self.braviarc.get_power_status()
+
+ if power_status != "off":
+ connected = self.braviarc.is_connected()
+ if not connected:
+ try:
+ connected = self.braviarc.connect(
+ self.pin, CLIENTID_PREFIX, NICKNAME
+ )
+ except NoIPControl:
+ _LOGGER.error("IP Control is disabled in the TV settings")
+ if not connected:
+ power_status = "off"
+
+ if power_status == "active":
+ self.is_on = True
+ if self._refresh_volume() and self._refresh_channels():
+ self._refresh_playing_info()
+ return
+
+ self.is_on = False
+
+ async def _async_update_data(self):
+ """Fetch the latest data."""
+ if self.state_lock.locked():
+ return
+
+ await self.hass.async_add_executor_job(self._update_tv_data)
+
+ async def async_turn_on(self):
+ """Turn the device on."""
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(self.braviarc.turn_on)
+ await self.async_request_refresh()
+
+ async def async_turn_off(self):
+ """Turn off device."""
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(self.braviarc.turn_off)
+ await self.async_request_refresh()
+
+ async def async_set_volume_level(self, volume):
+ """Set volume level, range 0..1."""
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(
+ self.braviarc.set_volume_level, volume, self.audio_output
+ )
+ await self.async_request_refresh()
+
+ async def async_volume_up(self):
+ """Send volume up command to device."""
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(
+ self.braviarc.volume_up, self.audio_output
+ )
+ await self.async_request_refresh()
+
+ async def async_volume_down(self):
+ """Send volume down command to device."""
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(
+ self.braviarc.volume_down, self.audio_output
+ )
+ await self.async_request_refresh()
+
+ async def async_volume_mute(self, mute):
+ """Send mute command to device."""
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(self.braviarc.mute_volume, mute)
+ await self.async_request_refresh()
+
+ async def async_media_play(self):
+ """Send play command to device."""
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(self.braviarc.media_play)
+ self.playing = True
+ await self.async_request_refresh()
+
+ async def async_media_pause(self):
+ """Send pause command to device."""
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(self.braviarc.media_pause)
+ self.playing = False
+ await self.async_request_refresh()
+
+ async def async_media_stop(self):
+ """Send stop command to device."""
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(self.braviarc.media_stop)
+ self.playing = False
+ await self.async_request_refresh()
+
+ async def async_media_next_track(self):
+ """Send next track command."""
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(self.braviarc.media_next_track)
+ await self.async_request_refresh()
+
+ async def async_media_previous_track(self):
+ """Send previous track command."""
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(self.braviarc.media_previous_track)
+ await self.async_request_refresh()
+
+ async def async_select_source(self, source):
+ """Set the input source."""
+ if source in self.content_mapping:
+ uri = self.content_mapping[source]
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(self.braviarc.play_content, uri)
+ await self.async_request_refresh()
+
+ async def async_send_command(self, command, repeats):
+ """Send command to device."""
+ async with self.state_lock:
+ await self.hass.async_add_executor_job(self._send_command, command, repeats)
+ await self.async_request_refresh()
diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py
index 02856887d17..0813a3e52c5 100644
--- a/homeassistant/components/braviatv/config_flow.py
+++ b/homeassistant/components/braviatv/config_flow.py
@@ -1,6 +1,5 @@
"""Adds config flow for Bravia TV integration."""
import ipaddress
-import logging
import re
from bravia_tv import BraviaRC
@@ -16,15 +15,12 @@ from .const import (
ATTR_CID,
ATTR_MAC,
ATTR_MODEL,
- BRAVIARC,
CLIENTID_PREFIX,
CONF_IGNORED_SOURCES,
DOMAIN,
NICKNAME,
)
-_LOGGER = logging.getLogger(__name__)
-
def host_valid(host):
"""Return True if hostname or IP address is valid."""
@@ -76,27 +72,6 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Bravia TV options callback."""
return BraviaTVOptionsFlowHandler(config_entry)
- async def async_step_import(self, user_input=None):
- """Handle configuration by yaml file."""
- self.host = user_input[CONF_HOST]
- self.braviarc = BraviaRC(self.host)
-
- try:
- await self.init_device(user_input[CONF_PIN])
- except CannotConnect:
- _LOGGER.error("Import aborted, cannot connect to %s", self.host)
- return self.async_abort(reason="cannot_connect")
- except NoIPControl:
- _LOGGER.error("IP Control is disabled in the TV settings")
- return self.async_abort(reason="no_ip_control")
- except ModelNotSupported:
- _LOGGER.error("Import aborted, your TV is not supported")
- return self.async_abort(reason="unsupported_model")
-
- user_input[CONF_MAC] = self.mac
-
- return self.async_create_entry(title=self.title, data=user_input)
-
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
@@ -160,7 +135,8 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(self, user_input=None):
"""Manage the options."""
- self.braviarc = self.hass.data[DOMAIN][self.config_entry.entry_id][BRAVIARC]
+ coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id]
+ self.braviarc = coordinator.braviarc
connected = await self.hass.async_add_executor_job(self.braviarc.is_connected)
if not connected:
await self.hass.async_add_executor_job(
diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py
index a5d7a88d4c3..1fa96e6a98d 100644
--- a/homeassistant/components/braviatv/const.py
+++ b/homeassistant/components/braviatv/const.py
@@ -6,10 +6,8 @@ ATTR_MODEL = "model"
CONF_IGNORED_SOURCES = "ignored_sources"
-BRAVIARC = "braviarc"
BRAVIA_CONFIG_FILE = "bravia.conf"
CLIENTID_PREFIX = "HomeAssistant"
DEFAULT_NAME = f"{ATTR_MANUFACTURER} Bravia TV"
DOMAIN = "braviatv"
NICKNAME = "Home Assistant"
-UNDO_UPDATE_LISTENER = "undo_update_listener"
diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json
index f7456c08c13..18285ebec00 100644
--- a/homeassistant/components/braviatv/manifest.json
+++ b/homeassistant/components/braviatv/manifest.json
@@ -3,7 +3,7 @@
"name": "Sony Bravia TV",
"documentation": "https://www.home-assistant.io/integrations/braviatv",
"requirements": ["bravia-tv==1.0.11"],
- "codeowners": ["@bieniu"],
+ "codeowners": ["@bieniu", "@Drafteed"],
"config_flow": true,
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py
index 14b47f95101..dda5b005497 100644
--- a/homeassistant/components/braviatv/media_player.py
+++ b/homeassistant/components/braviatv/media_player.py
@@ -1,15 +1,5 @@
"""Support for interface with a Bravia TV."""
-import asyncio
-import logging
-
-from bravia_tv.braviarc import NoIPControl
-import voluptuous as vol
-
-from homeassistant.components.media_player import (
- DEVICE_CLASS_TV,
- PLATFORM_SCHEMA,
- MediaPlayerEntity,
-)
+from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity
from homeassistant.components.media_player.const import (
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
@@ -23,23 +13,10 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP,
)
-from homeassistant.config_entries import SOURCE_IMPORT
-from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, STATE_OFF, STATE_ON
-import homeassistant.helpers.config_validation as cv
-from homeassistant.util.json import load_json
+from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from .const import (
- ATTR_MANUFACTURER,
- BRAVIA_CONFIG_FILE,
- BRAVIARC,
- CLIENTID_PREFIX,
- CONF_IGNORED_SOURCES,
- DEFAULT_NAME,
- DOMAIN,
- NICKNAME,
-)
-
-_LOGGER = logging.getLogger(__name__)
+from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN
SUPPORT_BRAVIA = (
SUPPORT_PAUSE
@@ -55,48 +32,11 @@ SUPPORT_BRAVIA = (
| SUPPORT_STOP
)
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- }
-)
-
-
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the Bravia TV platform."""
- host = config[CONF_HOST]
-
- bravia_config_file_path = hass.config.path(BRAVIA_CONFIG_FILE)
- bravia_config = await hass.async_add_executor_job(
- load_json, bravia_config_file_path
- )
- if not bravia_config:
- _LOGGER.error(
- "Configuration import failed, there is no bravia.conf file in the configuration folder"
- )
- return
-
- while bravia_config:
- # Import a configured TV
- host_ip, host_config = bravia_config.popitem()
- if host_ip == host:
- pin = host_config[CONF_PIN]
-
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_IMPORT},
- data={CONF_HOST: host, CONF_PIN: pin},
- )
- )
- return
-
async def async_setup_entry(hass, config_entry, async_add_entities):
- """Add BraviaTV entities from a config_entry."""
- ignored_sources = []
- pin = config_entry.data[CONF_PIN]
+ """Set up Bravia TV Media Player from a config_entry."""
+
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]
unique_id = config_entry.unique_id
device_info = {
"identifiers": {(DOMAIN, unique_id)},
@@ -105,264 +45,112 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"model": config_entry.title,
}
- braviarc = hass.data[DOMAIN][config_entry.entry_id][BRAVIARC]
-
- ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, [])
-
async_add_entities(
- [
- BraviaTVDevice(
- braviarc, DEFAULT_NAME, pin, unique_id, device_info, ignored_sources
- )
- ]
+ [BraviaTVMediaPlayer(coordinator, DEFAULT_NAME, unique_id, device_info)]
)
-class BraviaTVDevice(MediaPlayerEntity):
- """Representation of a Bravia TV."""
+class BraviaTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
+ """Representation of a Bravia TV Media Player."""
- def __init__(self, client, name, pin, unique_id, device_info, ignored_sources):
- """Initialize the Bravia TV device."""
+ _attr_device_class = DEVICE_CLASS_TV
+ _attr_supported_features = SUPPORT_BRAVIA
- self._pin = pin
- self._braviarc = client
- self._name = name
- self._state = STATE_OFF
- self._muted = False
- self._program_name = None
- self._channel_name = None
- self._channel_number = None
- self._source = None
- self._source_list = []
- self._original_content_list = []
- self._content_mapping = {}
- self._duration = None
- self._content_uri = None
- self._playing = False
- self._start_date_time = None
- self._program_media_type = None
- self._audio_output = None
- self._min_volume = None
- self._max_volume = None
- self._volume = None
- self._unique_id = unique_id
- self._device_info = device_info
- self._ignored_sources = ignored_sources
- self._state_lock = asyncio.Lock()
+ def __init__(self, coordinator, name, unique_id, device_info):
+ """Initialize the entity."""
- async def async_update(self):
- """Update TV info."""
- if self._state_lock.locked():
- return
+ self._attr_device_info = device_info
+ self._attr_name = name
+ self._attr_unique_id = unique_id
- power_status = await self.hass.async_add_executor_job(
- self._braviarc.get_power_status
- )
-
- if power_status != "off":
- connected = await self.hass.async_add_executor_job(
- self._braviarc.is_connected
- )
- if not connected:
- try:
- connected = await self.hass.async_add_executor_job(
- self._braviarc.connect, self._pin, CLIENTID_PREFIX, NICKNAME
- )
- except NoIPControl:
- _LOGGER.error("IP Control is disabled in the TV settings")
- if not connected:
- power_status = "off"
-
- if power_status == "active":
- self._state = STATE_ON
- if (
- await self._async_refresh_volume()
- and await self._async_refresh_channels()
- ):
- await self._async_refresh_playing_info()
- return
- self._state = STATE_OFF
-
- def _get_source(self):
- """Return the name of the source."""
- for key, value in self._content_mapping.items():
- if value == self._content_uri:
- return key
-
- async def _async_refresh_volume(self):
- """Refresh volume information."""
- volume_info = await self.hass.async_add_executor_job(
- self._braviarc.get_volume_info, self._audio_output
- )
- if volume_info is not None:
- self._audio_output = volume_info.get("target")
- self._volume = volume_info.get("volume")
- self._min_volume = volume_info.get("minVolume")
- self._max_volume = volume_info.get("maxVolume")
- self._muted = volume_info.get("mute")
- return True
- return False
-
- async def _async_refresh_channels(self):
- """Refresh source and channels list."""
- if not self._source_list:
- self._content_mapping = await self.hass.async_add_executor_job(
- self._braviarc.load_source_list
- )
- self._source_list = []
- if not self._content_mapping:
- return False
- for key in self._content_mapping:
- if key not in self._ignored_sources:
- self._source_list.append(key)
- return True
-
- async def _async_refresh_playing_info(self):
- """Refresh Playing information."""
- playing_info = await self.hass.async_add_executor_job(
- self._braviarc.get_playing_info
- )
- self._program_name = playing_info.get("programTitle")
- self._channel_name = playing_info.get("title")
- self._program_media_type = playing_info.get("programMediaType")
- self._channel_number = playing_info.get("dispNum")
- self._content_uri = playing_info.get("uri")
- self._source = self._get_source()
- self._duration = playing_info.get("durationSec")
- self._start_date_time = playing_info.get("startDateTime")
- if not playing_info:
- self._channel_name = "App"
-
- @property
- def name(self):
- """Return the name of the device."""
- return self._name
-
- @property
- def device_class(self):
- """Set the device class to TV."""
- return DEVICE_CLASS_TV
-
- @property
- def unique_id(self):
- """Return a unique_id for this entity."""
- return self._unique_id
-
- @property
- def device_info(self):
- """Return the device info."""
- return self._device_info
+ super().__init__(coordinator)
@property
def state(self):
"""Return the state of the device."""
- return self._state
+ if self.coordinator.is_on:
+ return STATE_PLAYING if self.coordinator.playing else STATE_PAUSED
+ return STATE_OFF
@property
def source(self):
"""Return the current input source."""
- return self._source
+ return self.coordinator.source
@property
def source_list(self):
"""List of available input sources."""
- return self._source_list
+ return self.coordinator.source_list
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
- if self._volume is not None:
- return self._volume / 100
- return None
+ return self.coordinator.volume_level
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
- return self._muted
-
- @property
- def supported_features(self):
- """Flag media player features that are supported."""
- return SUPPORT_BRAVIA
+ return self.coordinator.muted
@property
def media_title(self):
"""Title of current playing media."""
- return_value = None
- if self._channel_name is not None:
- return_value = self._channel_name
- if self._program_name is not None:
- return_value = f"{return_value}: {self._program_name}"
- return return_value
+ return self.coordinator.media_title
@property
def media_content_id(self):
"""Content ID of current playing media."""
- return self._channel_name
+ return self.coordinator.channel_name
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
- return self._duration
-
- def set_volume_level(self, volume):
- """Set volume level, range 0..1."""
- self._braviarc.set_volume_level(volume, self._audio_output)
+ return self.coordinator.duration
async def async_turn_on(self):
- """Turn the media player on."""
- async with self._state_lock:
- await self.hass.async_add_executor_job(self._braviarc.turn_on)
+ """Turn the device on."""
+ await self.coordinator.async_turn_on()
async def async_turn_off(self):
- """Turn off media player."""
- async with self._state_lock:
- await self.hass.async_add_executor_job(self._braviarc.turn_off)
+ """Turn the device off."""
+ await self.coordinator.async_turn_off()
- def volume_up(self):
- """Volume up the media player."""
- self._braviarc.volume_up(self._audio_output)
+ async def async_set_volume_level(self, volume):
+ """Set volume level, range 0..1."""
+ await self.coordinator.async_set_volume_level(volume)
- def volume_down(self):
- """Volume down media player."""
- self._braviarc.volume_down(self._audio_output)
+ async def async_volume_up(self):
+ """Send volume up command."""
+ await self.coordinator.async_volume_up()
- def mute_volume(self, mute):
+ async def async_volume_down(self):
+ """Send volume down command."""
+ await self.coordinator.async_volume_down()
+
+ async def async_mute_volume(self, mute):
"""Send mute command."""
- self._braviarc.mute_volume(mute)
+ await self.coordinator.async_volume_mute(mute)
- def select_source(self, source):
+ async def async_select_source(self, source):
"""Set the input source."""
- if source in self._content_mapping:
- uri = self._content_mapping[source]
- self._braviarc.play_content(uri)
+ await self.coordinator.async_select_source(source)
- def media_play_pause(self):
- """Simulate play pause media player."""
- if self._playing:
- self.media_pause()
- else:
- self.media_play()
-
- def media_play(self):
+ async def async_media_play(self):
"""Send play command."""
- self._playing = True
- self._braviarc.media_play()
+ await self.coordinator.async_media_play()
- def media_pause(self):
- """Send media pause command to media player."""
- self._playing = False
- self._braviarc.media_pause()
+ async def async_media_pause(self):
+ """Send pause command."""
+ await self.coordinator.async_media_pause()
- def media_stop(self):
+ async def async_media_stop(self):
"""Send media stop command to media player."""
- self._playing = False
- self._braviarc.media_stop()
+ await self.coordinator.async_media_stop()
- def media_next_track(self):
+ async def async_media_next_track(self):
"""Send next track command."""
- self._braviarc.media_next_track()
+ await self.coordinator.async_media_next_track()
- def media_previous_track(self):
- """Send the previous track command."""
- self._braviarc.media_previous_track()
+ async def async_media_previous_track(self):
+ """Send previous track command."""
+ await self.coordinator.async_media_previous_track()
diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py
new file mode 100644
index 00000000000..613d67f0187
--- /dev/null
+++ b/homeassistant/components/braviatv/remote.py
@@ -0,0 +1,54 @@
+"""Remote control support for Bravia TV."""
+
+from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Bravia TV Remote from a config entry."""
+
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]
+ unique_id = config_entry.unique_id
+ device_info = {
+ "identifiers": {(DOMAIN, unique_id)},
+ "name": DEFAULT_NAME,
+ "manufacturer": ATTR_MANUFACTURER,
+ "model": config_entry.title,
+ }
+
+ async_add_entities(
+ [BraviaTVRemote(coordinator, DEFAULT_NAME, unique_id, device_info)]
+ )
+
+
+class BraviaTVRemote(CoordinatorEntity, RemoteEntity):
+ """Representation of a Bravia TV Remote."""
+
+ def __init__(self, coordinator, name, unique_id, device_info):
+ """Initialize the entity."""
+
+ self._attr_device_info = device_info
+ self._attr_name = name
+ self._attr_unique_id = unique_id
+
+ super().__init__(coordinator)
+
+ @property
+ def is_on(self):
+ """Return true if device is on."""
+ return self.coordinator.is_on
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the device on."""
+ await self.coordinator.async_turn_on()
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the device off."""
+ await self.coordinator.async_turn_off()
+
+ async def async_send_command(self, command, **kwargs):
+ """Send a command to device."""
+ repeats = kwargs[ATTR_NUM_REPEATS]
+ await self.coordinator.async_send_command(command, repeats)
diff --git a/homeassistant/components/braviatv/translations/he.json b/homeassistant/components/braviatv/translations/he.json
new file mode 100644
index 00000000000..ab9d638a8ac
--- /dev/null
+++ b/homeassistant/components/braviatv/translations/he.json
@@ -0,0 +1,23 @@
+{
+ "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",
+ "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd"
+ },
+ "step": {
+ "authorize": {
+ "data": {
+ "pin": "\u05e7\u05d5\u05d3 PIN"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py
index b18d64c327f..2686b3dd9ed 100644
--- a/homeassistant/components/broadlink/device.py
+++ b/homeassistant/components/broadlink/device.py
@@ -51,6 +51,11 @@ class BroadlinkDevice:
"""Return the unique id of the device."""
return self.config.unique_id
+ @property
+ def mac_address(self):
+ """Return the mac address of the device."""
+ return self.config.data[CONF_MAC]
+
@staticmethod
async def async_update(hass, entry):
"""Update the device and related entities.
diff --git a/homeassistant/components/broadlink/entity.py b/homeassistant/components/broadlink/entity.py
new file mode 100644
index 00000000000..850611b391f
--- /dev/null
+++ b/homeassistant/components/broadlink/entity.py
@@ -0,0 +1,32 @@
+"""Broadlink entities."""
+
+from homeassistant.helpers import device_registry as dr
+
+from .const import DOMAIN
+
+
+class BroadlinkEntity:
+ """Representation of a Broadlink entity."""
+
+ _attr_should_poll = False
+
+ def __init__(self, device):
+ """Initialize the device."""
+ self._device = device
+
+ @property
+ def available(self):
+ """Return True if the remote is available."""
+ return self._device.update_manager.available
+
+ @property
+ def device_info(self):
+ """Return device info."""
+ return {
+ "identifiers": {(DOMAIN, self._device.unique_id)},
+ "connections": {(dr.CONNECTION_NETWORK_MAC, self._device.mac_address)},
+ "manufacturer": self._device.api.manufacturer,
+ "model": self._device.api.model,
+ "name": self._device.name,
+ "sw_version": self._device.fw_version,
+ }
diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py
index 291bf6a3d8b..b9dd34d22d8 100644
--- a/homeassistant/components/broadlink/remote.py
+++ b/homeassistant/components/broadlink/remote.py
@@ -40,6 +40,7 @@ from homeassistant.helpers.storage import Store
from homeassistant.util.dt import utcnow
from .const import DOMAIN
+from .entity import BroadlinkEntity
from .helpers import data_packet, import_device
_LOGGER = logging.getLogger(__name__)
@@ -112,61 +113,24 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities([remote], False)
-class BroadlinkRemote(RemoteEntity, RestoreEntity):
+class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
"""Representation of a Broadlink remote."""
def __init__(self, device, codes, flags):
"""Initialize the remote."""
- self._device = device
+ super().__init__(device)
self._coordinator = device.update_manager.coordinator
self._code_storage = codes
self._flag_storage = flags
self._storage_loaded = False
self._codes = {}
self._flags = defaultdict(int)
- self._state = True
self._lock = asyncio.Lock()
- @property
- def name(self):
- """Return the name of the remote."""
- return f"{self._device.name} Remote"
-
- @property
- def unique_id(self):
- """Return the unique id of the remote."""
- return self._device.unique_id
-
- @property
- def is_on(self):
- """Return True if the remote is on."""
- return self._state
-
- @property
- def available(self):
- """Return True if the remote is available."""
- return self._device.update_manager.available
-
- @property
- def should_poll(self):
- """Return True if the remote has to be polled for state."""
- return False
-
- @property
- def supported_features(self):
- """Flag supported features."""
- return SUPPORT_LEARN_COMMAND | SUPPORT_DELETE_COMMAND
-
- @property
- def device_info(self):
- """Return device info."""
- return {
- "identifiers": {(DOMAIN, self._device.unique_id)},
- "manufacturer": self._device.api.manufacturer,
- "model": self._device.api.model,
- "name": self._device.name,
- "sw_version": self._device.fw_version,
- }
+ self._attr_name = f"{self._device.name} Remote"
+ self._attr_is_on = True
+ self._attr_supported_features = SUPPORT_LEARN_COMMAND | SUPPORT_DELETE_COMMAND
+ self._attr_unique_id = self._device.unique_id
def _extract_codes(self, commands, device=None):
"""Extract a list of codes.
@@ -224,7 +188,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity):
async def async_added_to_hass(self):
"""Call when the remote is added to hass."""
state = await self.async_get_last_state()
- self._state = state is None or state.state != STATE_OFF
+ self._attr_is_on = state is None or state.state != STATE_OFF
self.async_on_remove(
self._coordinator.async_add_listener(self.async_write_ha_state)
@@ -236,12 +200,12 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity):
async def async_turn_on(self, **kwargs):
"""Turn on the remote."""
- self._state = True
+ self._attr_is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""Turn off the remote."""
- self._state = False
+ self._attr_is_on = False
self.async_write_ha_state()
async def _async_load_storage(self):
@@ -262,7 +226,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity):
delay = kwargs[ATTR_DELAY_SECS]
service = f"{RM_DOMAIN}.{SERVICE_SEND_COMMAND}"
- if not self._state:
+ if not self._attr_is_on:
_LOGGER.warning(
"%s canceled: %s entity is turned off", service, self.entity_id
)
@@ -317,7 +281,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity):
toggle = kwargs[ATTR_ALTERNATIVE]
service = f"{RM_DOMAIN}.{SERVICE_LEARN_COMMAND}"
- if not self._state:
+ if not self._attr_is_on:
_LOGGER.warning(
"%s canceled: %s entity is turned off", service, self.entity_id
)
@@ -475,7 +439,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity):
device = kwargs[ATTR_DEVICE]
service = f"{RM_DOMAIN}.{SERVICE_DELETE_COMMAND}"
- if not self._state:
+ if not self._attr_is_on:
_LOGGER.warning(
"%s canceled: %s entity is turned off",
service,
diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py
index 92708583c43..851668fdeff 100644
--- a/homeassistant/components/broadlink/sensor.py
+++ b/homeassistant/components/broadlink/sensor.py
@@ -16,6 +16,7 @@ from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
+from .entity import BroadlinkEntity
from .helpers import import_device
_LOGGER = logging.getLogger(__name__)
@@ -67,72 +68,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors)
-class BroadlinkSensor(SensorEntity):
+class BroadlinkSensor(BroadlinkEntity, SensorEntity):
"""Representation of a Broadlink sensor."""
def __init__(self, device, monitored_condition):
"""Initialize the sensor."""
- self._device = device
+ super().__init__(device)
self._coordinator = device.update_manager.coordinator
self._monitored_condition = monitored_condition
- self._state = self._coordinator.data[monitored_condition]
- @property
- def unique_id(self):
- """Return the unique id of the sensor."""
- return f"{self._device.unique_id}-{self._monitored_condition}"
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return f"{self._device.name} {SENSOR_TYPES[self._monitored_condition][0]}"
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def available(self):
- """Return True if the sensor is available."""
- return self._device.update_manager.available
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of the sensor."""
- return SENSOR_TYPES[self._monitored_condition][1]
-
- @property
- def should_poll(self):
- """Return True if the sensor has to be polled for state."""
- return False
-
- @property
- def device_class(self):
- """Return device class."""
- return SENSOR_TYPES[self._monitored_condition][2]
-
- @property
- def state_class(self):
- """Return state class."""
- return SENSOR_TYPES[self._monitored_condition][3]
-
- @property
- def device_info(self):
- """Return device info."""
- return {
- "identifiers": {(DOMAIN, self._device.unique_id)},
- "manufacturer": self._device.api.manufacturer,
- "model": self._device.api.model,
- "name": self._device.name,
- "sw_version": self._device.fw_version,
- }
+ self._attr_device_class = SENSOR_TYPES[self._monitored_condition][2]
+ self._attr_name = (
+ f"{self._device.name} {SENSOR_TYPES[self._monitored_condition][0]}"
+ )
+ self._attr_state_class = SENSOR_TYPES[self._monitored_condition][3]
+ self._attr_state = self._coordinator.data[monitored_condition]
+ self._attr_unique_id = f"{self._device.unique_id}-{self._monitored_condition}"
+ self._attr_unit_of_measurement = SENSOR_TYPES[self._monitored_condition][1]
@callback
def update_data(self):
"""Update data."""
if self._coordinator.last_update_success:
- self._state = self._coordinator.data[self._monitored_condition]
+ self._attr_state = self._coordinator.data[self._monitored_condition]
self.async_write_ha_state()
async def async_added_to_hass(self):
diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py
index 0a98530c806..1576c8b8418 100644
--- a/homeassistant/components/broadlink/switch.py
+++ b/homeassistant/components/broadlink/switch.py
@@ -29,6 +29,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
from .const import DOMAIN, SWITCH_DOMAIN
+from .entity import BroadlinkEntity
from .helpers import data_packet, import_device, mac_address
_LOGGER = logging.getLogger(__name__)
@@ -131,58 +132,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(switches)
-class BroadlinkSwitch(SwitchEntity, RestoreEntity, ABC):
+class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC):
"""Representation of a Broadlink switch."""
def __init__(self, device, command_on, command_off):
"""Initialize the switch."""
- self._device = device
+ super().__init__(device)
self._command_on = command_on
self._command_off = command_off
self._coordinator = device.update_manager.coordinator
self._state = None
- @property
- def name(self):
- """Return the name of the switch."""
- return f"{self._device.name} Switch"
-
- @property
- def assumed_state(self):
- """Return True if unable to access real state of the switch."""
- return True
-
- @property
- def available(self):
- """Return True if the switch is available."""
- return self._device.update_manager.available
+ self._attr_assumed_state = True
+ self._attr_device_class = DEVICE_CLASS_SWITCH
+ self._attr_name = f"{self._device.name} Switch"
@property
def is_on(self):
"""Return True if the switch is on."""
return self._state
- @property
- def should_poll(self):
- """Return True if the switch has to be polled for state."""
- return False
-
- @property
- def device_class(self):
- """Return device class."""
- return DEVICE_CLASS_SWITCH
-
- @property
- def device_info(self):
- """Return device info."""
- return {
- "identifiers": {(DOMAIN, self._device.unique_id)},
- "manufacturer": self._device.api.manufacturer,
- "model": self._device.api.model,
- "name": self._device.name,
- "sw_version": self._device.fw_version,
- }
-
@callback
def update_data(self):
"""Update data."""
@@ -224,12 +193,7 @@ class BroadlinkRMSwitch(BroadlinkSwitch):
super().__init__(
device, config.get(CONF_COMMAND_ON), config.get(CONF_COMMAND_OFF)
)
- self._name = config[CONF_NAME]
-
- @property
- def name(self):
- """Return the name of the switch."""
- return self._name
+ self._attr_name = config[CONF_NAME]
async def _async_send_packet(self, packet):
"""Send a packet to the device."""
@@ -250,11 +214,7 @@ class BroadlinkSP1Switch(BroadlinkSwitch):
def __init__(self, device):
"""Initialize the switch."""
super().__init__(device, 1, 0)
-
- @property
- def unique_id(self):
- """Return the unique id of the switch."""
- return self._device.unique_id
+ self._attr_unique_id = self._device.unique_id
async def _async_send_packet(self, packet):
"""Send a packet to the device."""
@@ -275,10 +235,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
self._state = self._coordinator.data["pwr"]
self._load_power = self._coordinator.data.get("power")
- @property
- def assumed_state(self):
- """Return True if unable to access real state of the switch."""
- return False
+ self._attr_assumed_state = False
@property
def current_power_w(self):
@@ -303,20 +260,9 @@ class BroadlinkMP1Slot(BroadlinkSwitch):
self._slot = slot
self._state = self._coordinator.data[f"s{slot}"]
- @property
- def unique_id(self):
- """Return the unique id of the slot."""
- return f"{self._device.unique_id}-s{self._slot}"
-
- @property
- def name(self):
- """Return the name of the switch."""
- return f"{self._device.name} S{self._slot}"
-
- @property
- def assumed_state(self):
- """Return True if unable to access real state of the switch."""
- return False
+ self._attr_name = f"{self._device.name} S{self._slot}"
+ self._attr_unique_id = f"{self._device.unique_id}-s{self._slot}"
+ self._attr_assumed_state = False
@callback
def update_data(self):
@@ -346,25 +292,10 @@ class BroadlinkBG1Slot(BroadlinkSwitch):
self._slot = slot
self._state = self._coordinator.data[f"pwr{slot}"]
- @property
- def unique_id(self):
- """Return the unique id of the slot."""
- return f"{self._device.unique_id}-s{self._slot}"
-
- @property
- def name(self):
- """Return the name of the switch."""
- return f"{self._device.name} S{self._slot}"
-
- @property
- def assumed_state(self):
- """Return True if unable to access real state of the switch."""
- return False
-
- @property
- def device_class(self):
- """Return device class."""
- return DEVICE_CLASS_OUTLET
+ self._attr_name = f"{self._device.name} S{self._slot}"
+ self._attr_device_class = DEVICE_CLASS_OUTLET
+ self._attr_unique_id = f"{self._device.unique_id}-s{self._slot}"
+ self._attr_assumed_state = False
@callback
def update_data(self):
diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json
index d1ab0987a3a..d81c131bf5d 100644
--- a/homeassistant/components/broadlink/translations/de.json
+++ b/homeassistant/components/broadlink/translations/de.json
@@ -13,6 +13,7 @@
"invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse",
"unknown": "Unerwarteter Fehler"
},
+ "flow_title": "{name} ({model} unter {host})",
"step": {
"auth": {
"title": "Authentifiziere dich beim Ger\u00e4t"
@@ -24,13 +25,22 @@
"title": "W\u00e4hle einen Namen f\u00fcr das Ger\u00e4t"
},
"reset": {
- "description": "{Name} ({Modell} unter {Host}) ist gesperrt. Du musst das Ger\u00e4t entsperren, um dich zu authentifizieren und die Konfiguration abzuschlie\u00dfen. Anweisungen:\n1. \u00d6ffne die Broadlink-App.\n2. Klicke auf auf das Ger\u00e4t.\n3. Klicke oben rechts auf `...`.\n4. Scrolle zum unteren Ende der Seite.\n5. Deaktiviere die Sperre."
+ "description": "{name} ({model} unter {host}) ist gesperrt. Du musst das Ger\u00e4t entsperren, um dich zu authentifizieren und die Konfiguration abzuschlie\u00dfen. Anweisungen:\n1. \u00d6ffne die Broadlink-App.\n2. Klicke auf auf das Ger\u00e4t.\n3. Klicke oben rechts auf `...`.\n4. Scrolle zum unteren Ende der Seite.\n5. Deaktiviere die Sperre.",
+ "title": "Entsperren des Ger\u00e4ts"
+ },
+ "unlock": {
+ "data": {
+ "unlock": "Ja mach das."
+ },
+ "description": "{name} ({model} unter {host}) ist gesperrt. Dies kann zu Authentifizierungsproblemen im Home Assistant f\u00fchren. M\u00f6chten Sie es entsperren?",
+ "title": "Entsperren des Ger\u00e4ts (optional)"
},
"user": {
"data": {
"host": "Host",
"timeout": "Zeit\u00fcberschreitung"
- }
+ },
+ "title": "Verbinden mit dem Ger\u00e4t"
}
}
}
diff --git a/homeassistant/components/broadlink/translations/he.json b/homeassistant/components/broadlink/translations/he.json
new file mode 100644
index 00000000000..a99f2f98761
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/he.json
@@ -0,0 +1,29 @@
+{
+ "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",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "flow_title": "{name} ({model} \u05d1-{host})",
+ "step": {
+ "finish": {
+ "data": {
+ "name": "\u05e9\u05dd"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py
index 353c8b05ed5..08daf0155a1 100644
--- a/homeassistant/components/brother/config_flow.py
+++ b/homeassistant/components/brother/config_flow.py
@@ -30,9 +30,9 @@ def host_valid(host: str) -> bool:
if ipaddress.ip_address(host).version in [4, 6]:
return True
except ValueError:
- disallowed = re.compile(r"[^a-zA-Z\d\-]")
- return all(x and not disallowed.search(x) for x in host.split("."))
- return False
+ pass
+ disallowed = re.compile(r"[^a-zA-Z\d\-]")
+ return all(x and not disallowed.search(x) for x in host.split("."))
class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py
index 52170057fb1..c0021df11fc 100644
--- a/homeassistant/components/brother/const.py
+++ b/homeassistant/components/brother/const.py
@@ -3,7 +3,13 @@ from __future__ import annotations
from typing import Final
-from homeassistant.const import ATTR_ICON, PERCENTAGE
+from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_ICON,
+ DEVICE_CLASS_TIMESTAMP,
+ PERCENTAGE,
+)
from .model import SensorDescription
@@ -84,143 +90,168 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
ATTR_LABEL: ATTR_STATUS.title(),
ATTR_UNIT: None,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: None,
},
ATTR_PAGE_COUNTER: {
ATTR_ICON: "mdi:file-document-outline",
ATTR_LABEL: ATTR_PAGE_COUNTER.replace("_", " ").title(),
ATTR_UNIT: UNIT_PAGES,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_BW_COUNTER: {
ATTR_ICON: "mdi:file-document-outline",
ATTR_LABEL: ATTR_BW_COUNTER.replace("_", " ").title(),
ATTR_UNIT: UNIT_PAGES,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_COLOR_COUNTER: {
ATTR_ICON: "mdi:file-document-outline",
ATTR_LABEL: ATTR_COLOR_COUNTER.replace("_", " ").title(),
ATTR_UNIT: UNIT_PAGES,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_DUPLEX_COUNTER: {
ATTR_ICON: "mdi:file-document-outline",
ATTR_LABEL: ATTR_DUPLEX_COUNTER.replace("_", " ").title(),
ATTR_UNIT: UNIT_PAGES,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_DRUM_REMAINING_LIFE: {
ATTR_ICON: "mdi:chart-donut",
ATTR_LABEL: ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_BLACK_DRUM_REMAINING_LIFE: {
ATTR_ICON: "mdi:chart-donut",
ATTR_LABEL: ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_CYAN_DRUM_REMAINING_LIFE: {
ATTR_ICON: "mdi:chart-donut",
ATTR_LABEL: ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_MAGENTA_DRUM_REMAINING_LIFE: {
ATTR_ICON: "mdi:chart-donut",
ATTR_LABEL: ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_YELLOW_DRUM_REMAINING_LIFE: {
ATTR_ICON: "mdi:chart-donut",
ATTR_LABEL: ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_BELT_UNIT_REMAINING_LIFE: {
ATTR_ICON: "mdi:current-ac",
ATTR_LABEL: ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_FUSER_REMAINING_LIFE: {
ATTR_ICON: "mdi:water-outline",
ATTR_LABEL: ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_LASER_REMAINING_LIFE: {
ATTR_ICON: "mdi:spotlight-beam",
ATTR_LABEL: ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_PF_KIT_1_REMAINING_LIFE: {
ATTR_ICON: "mdi:printer-3d",
ATTR_LABEL: ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_PF_KIT_MP_REMAINING_LIFE: {
ATTR_ICON: "mdi:printer-3d",
ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_BLACK_TONER_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_CYAN_TONER_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_MAGENTA_TONER_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_YELLOW_TONER_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_BLACK_INK_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_BLACK_INK_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_CYAN_INK_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_CYAN_INK_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_MAGENTA_INK_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_YELLOW_INK_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
ATTR_UPTIME: {
ATTR_ICON: None,
ATTR_LABEL: ATTR_UPTIME.title(),
ATTR_UNIT: None,
ATTR_ENABLED: False,
+ ATTR_STATE_CLASS: None,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
},
}
diff --git a/homeassistant/components/brother/model.py b/homeassistant/components/brother/model.py
index 22aa95eda50..ab8df09b749 100644
--- a/homeassistant/components/brother/model.py
+++ b/homeassistant/components/brother/model.py
@@ -4,10 +4,12 @@ from __future__ import annotations
from typing import TypedDict
-class SensorDescription(TypedDict):
+class SensorDescription(TypedDict, total=False):
"""Sensor description class."""
icon: str | None
label: str
unit: str | None
enabled: bool
+ state_class: str | None
+ device_class: str | None
diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py
index 50c9b8d79ff..38fac529076 100644
--- a/homeassistant/components/brother/sensor.py
+++ b/homeassistant/components/brother/sensor.py
@@ -3,9 +3,9 @@ from __future__ import annotations
from typing import Any
-from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_ICON, DEVICE_CLASS_TIMESTAMP
+from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -60,17 +60,17 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity):
) -> None:
"""Initialize."""
super().__init__(coordinator)
- self._description = SENSOR_TYPES[kind]
- self._name = f"{coordinator.data.model} {self._description[ATTR_LABEL]}"
- self._unique_id = f"{coordinator.data.serial.lower()}_{kind}"
- self._device_info = device_info
- self.kind = kind
+ description = SENSOR_TYPES[kind]
self._attrs: dict[str, Any] = {}
-
- @property
- def name(self) -> str:
- """Return the name."""
- return self._name
+ self._attr_device_class = description.get(ATTR_DEVICE_CLASS)
+ self._attr_device_info = device_info
+ self._attr_entity_registry_enabled_default = description[ATTR_ENABLED]
+ self._attr_icon = description[ATTR_ICON]
+ self._attr_name = f"{coordinator.data.model} {description[ATTR_LABEL]}"
+ self._attr_state_class = description[ATTR_STATE_CLASS]
+ self._attr_unique_id = f"{coordinator.data.serial.lower()}_{kind}"
+ self._attr_unit_of_measurement = description[ATTR_UNIT]
+ self.kind = kind
@property
def state(self) -> Any:
@@ -79,13 +79,6 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity):
return getattr(self.coordinator.data, self.kind).isoformat()
return getattr(self.coordinator.data, self.kind)
- @property
- def device_class(self) -> str | None:
- """Return the class of this sensor."""
- if self.kind == ATTR_UPTIME:
- return DEVICE_CLASS_TIMESTAMP
- return None
-
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
@@ -96,28 +89,3 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity):
)
self._attrs[ATTR_COUNTER] = getattr(self.coordinator.data, drum_counter)
return self._attrs
-
- @property
- def icon(self) -> str | None:
- """Return the icon."""
- return self._description[ATTR_ICON]
-
- @property
- def unique_id(self) -> str:
- """Return a unique_id for this entity."""
- return self._unique_id
-
- @property
- def unit_of_measurement(self) -> str | None:
- """Return the unit the value is expressed in."""
- return self._description[ATTR_UNIT]
-
- @property
- def device_info(self) -> DeviceInfo:
- """Return the device info."""
- return self._device_info
-
- @property
- def entity_registry_enabled_default(self) -> bool:
- """Return if the entity should be enabled when first added to the entity registry."""
- return self._description[ATTR_ENABLED]
diff --git a/homeassistant/components/brother/translations/de.json b/homeassistant/components/brother/translations/de.json
index c2a7ae8ec76..3390ca6ca8f 100644
--- a/homeassistant/components/brother/translations/de.json
+++ b/homeassistant/components/brother/translations/de.json
@@ -9,7 +9,7 @@
"snmp_error": "SNMP-Server deaktiviert oder Drucker nicht unterst\u00fctzt.",
"wrong_host": " Ung\u00fcltiger Hostname oder IP-Adresse"
},
- "flow_title": "Brother-Drucker: {model} {serial_number}",
+ "flow_title": "{model} {serial_number}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/brother/translations/he.json b/homeassistant/components/brother/translations/he.json
new file mode 100644
index 00000000000..af3f5750ddb
--- /dev/null
+++ b/homeassistant/components/brother/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "wrong_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd."
+ },
+ "flow_title": "{model} {serial_number}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json
index d1400529b0b..ce9d8a0cb00 100644
--- a/homeassistant/components/bsblan/translations/de.json
+++ b/homeassistant/components/bsblan/translations/de.json
@@ -6,7 +6,7 @@
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
},
- "flow_title": "BSB-Lan: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/bsblan/translations/he.json b/homeassistant/components/bsblan/translations/he.json
new file mode 100644
index 00000000000..2c183d1ac24
--- /dev/null
+++ b/homeassistant/components/bsblan/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "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"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bsblan/translations/hu.json b/homeassistant/components/bsblan/translations/hu.json
index 50d250cc384..499a7d92331 100644
--- a/homeassistant/components/bsblan/translations/hu.json
+++ b/homeassistant/components/bsblan/translations/hu.json
@@ -6,7 +6,7 @@
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
- "flow_title": "BSB-Lan: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py
index 1a2d6d4d0be..059cd79d522 100644
--- a/homeassistant/components/buienradar/camera.py
+++ b/homeassistant/components/buienradar/camera.py
@@ -11,10 +11,10 @@ import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import dt as dt_util
from .const import (
@@ -56,7 +56,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(
- hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up buienradar radar-loop camera component."""
config = entry.data
diff --git a/homeassistant/components/buienradar/translations/de.json b/homeassistant/components/buienradar/translations/de.json
index 72f1ebfed3c..bb09a98617d 100644
--- a/homeassistant/components/buienradar/translations/de.json
+++ b/homeassistant/components/buienradar/translations/de.json
@@ -20,7 +20,8 @@
"init": {
"data": {
"country_code": "L\u00e4ndercode des Landes, in dem Kamerabilder angezeigt werden sollen.",
- "delta": "Zeitintervall in Sekunden zwischen Kamerabildaktualisierungen"
+ "delta": "Zeitintervall in Sekunden zwischen Kamerabildaktualisierungen",
+ "timeframe": "Minuten zum Vorausschauen f\u00fcr die Niederschlagsvorhersage"
}
}
}
diff --git a/homeassistant/components/buienradar/translations/he.json b/homeassistant/components/buienradar/translations/he.json
new file mode 100644
index 00000000000..76da9d34ddf
--- /dev/null
+++ b/homeassistant/components/buienradar/translations/he.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py
index 62be361df3b..61186249c51 100644
--- a/homeassistant/components/caldav/calendar.py
+++ b/homeassistant/components/caldav/calendar.py
@@ -310,7 +310,9 @@ class WebDavCalendarData:
# represent same time regardless of which time zone is currently being observed
return obj.replace(tzinfo=dt.DEFAULT_TIME_ZONE)
return obj
- return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min))
+ return dt.dt.datetime.combine(obj, dt.dt.time.min).replace(
+ tzinfo=dt.DEFAULT_TIME_ZONE
+ )
@staticmethod
def get_attr_value(obj, attribute):
diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py
index 11a6916ba83..8809e05d25b 100644
--- a/homeassistant/components/calendar/__init__.py
+++ b/homeassistant/components/calendar/__init__.py
@@ -9,7 +9,9 @@ from typing import cast, final
from aiohttp import web
from homeassistant.components import http
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import HTTP_BAD_REQUEST, STATE_OFF, STATE_ON
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
@@ -46,14 +48,16 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
def get_date(date):
diff --git a/homeassistant/components/camera/translations/he.json b/homeassistant/components/camera/translations/he.json
index ccca3a79099..ca6b207762d 100644
--- a/homeassistant/components/camera/translations/he.json
+++ b/homeassistant/components/camera/translations/he.json
@@ -6,5 +6,5 @@
"streaming": "\u05de\u05d6\u05e8\u05d9\u05dd"
}
},
- "title": "\u05de\u05b7\u05e6\u05dc\u05b5\u05de\u05b8\u05d4"
+ "title": "\u05de\u05e6\u05dc\u05de\u05d4"
}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/de.json b/homeassistant/components/canary/translations/de.json
index bdd746c3149..93ce43c61f5 100644
--- a/homeassistant/components/canary/translations/de.json
+++ b/homeassistant/components/canary/translations/de.json
@@ -7,13 +7,14 @@
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
},
- "flow_title": "Canary: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
"password": "Passwort",
"username": "Benutzername"
- }
+ },
+ "title": "Mit Canary verbinden"
}
}
},
@@ -21,6 +22,7 @@
"step": {
"init": {
"data": {
+ "ffmpeg_arguments": "An ffmpeg \u00fcbergebene Argumente f\u00fcr Kameras",
"timeout": "Anfrage-Timeout (Sekunden)"
}
}
diff --git a/homeassistant/components/canary/translations/he.json b/homeassistant/components/canary/translations/he.json
new file mode 100644
index 00000000000..fbea60c3704
--- /dev/null
+++ b/homeassistant/components/canary/translations/he.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d1\u05e7\u05e9\u05d4 (\u05e9\u05e0\u05d9\u05d5\u05ea)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/hu.json b/homeassistant/components/canary/translations/hu.json
index c2c70fdbf22..85dd503a175 100644
--- a/homeassistant/components/canary/translations/hu.json
+++ b/homeassistant/components/canary/translations/hu.json
@@ -7,7 +7,7 @@
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
- "flow_title": "Canary: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json
index c104ff7a12e..78f7bcf485c 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==9.1.2"],
+ "requirements": ["pychromecast==9.2.0"],
"after_dependencies": [
"cloud",
"http",
diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py
index 969e690fcc2..07e97dd1a7e 100644
--- a/homeassistant/components/cast/media_player.py
+++ b/homeassistant/components/cast/media_player.py
@@ -482,10 +482,15 @@ class CastDevice(MediaPlayerEntity):
def play_media(self, media_type, media_id, **kwargs):
"""Play media from a URL."""
+ extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
+ metadata = extra.get("metadata")
+
# We do not want this to be forwarded to a group
if media_type == CAST_DOMAIN:
try:
app_data = json.loads(media_id)
+ if metadata is not None:
+ app_data["metadata"] = extra.get("metadata")
except json.JSONDecodeError:
_LOGGER.error("Invalid JSON in media_content_id")
raise
diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json
index 1358ab16210..4d029be9603 100644
--- a/homeassistant/components/cast/translations/de.json
+++ b/homeassistant/components/cast/translations/de.json
@@ -29,7 +29,7 @@
"ignore_cec": "CEC ignorieren",
"uuid": "Zul\u00e4ssige UUIDs"
},
- "description": "Erlaubte UUIDs - Eine kommagetrennte Liste von UUIDs von Cast-Ger\u00e4ten, die dem Home Assistant hinzugef\u00fcgt werden sollen. Nur verwenden, wenn Sie nicht alle verf\u00fcgbaren Cast-Ger\u00e4te hinzuf\u00fcgen m\u00f6chten.\nIgnore CEC - Eine kommagetrennte Liste von Chromecasts, die CEC-Daten zur Bestimmung des aktiven Eingangs ignorieren sollen. Dies wird an pychromecast.IGNORE_CEC \u00fcbergeben.",
+ "description": "Erlaubte UUIDs - Eine kommagetrennte Liste von UUIDs von Cast-Ger\u00e4ten, die dem Home Assistant hinzugef\u00fcgt werden sollen. Nur verwenden, wenn Sie nicht alle verf\u00fcgbaren Cast-Ger\u00e4te hinzuf\u00fcgen m\u00f6chten.\nCEC ignorieren - Eine kommagetrennte Liste von Chromecasts, die CEC-Daten zur Bestimmung des aktiven Eingangs ignorieren sollen. Dies wird an pychromecast.IGNORE_CEC \u00fcbergeben.",
"title": "Erweiterte Google Cast-Konfiguration"
},
"basic_options": {
diff --git a/homeassistant/components/cast/translations/he.json b/homeassistant/components/cast/translations/he.json
index 09c85008e10..1d3af8b2718 100644
--- a/homeassistant/components/cast/translations/he.json
+++ b/homeassistant/components/cast/translations/he.json
@@ -3,10 +3,32 @@
"abort": {
"single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc Google Cast \u05e0\u05d7\u05d5\u05e6\u05d4."
},
+ "error": {
+ "invalid_known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd \u05d7\u05d9\u05d9\u05d1\u05d9\u05dd \u05dc\u05d4\u05d9\u05d5\u05ea \u05e8\u05e9\u05d9\u05de\u05ea \u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd."
+ },
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd"
+ },
+ "description": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd - \u05e8\u05e9\u05d9\u05de\u05d4 \u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd \u05e9\u05dc \u05e9\u05de\u05d5\u05ea \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05d5\u05ea IP \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d9\u05e6\u05d5\u05e7\u05d9\u05dd, \u05d4\u05e9\u05ea\u05de\u05e9 \u05d0\u05dd \u05d2\u05d9\u05dc\u05d5\u05d9 mDNS \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc."
+ },
"confirm": {
"description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Google Cast?"
}
}
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd \u05d7\u05d9\u05d9\u05d1\u05d9\u05dd \u05dc\u05d4\u05d9\u05d5\u05ea \u05e8\u05e9\u05d9\u05de\u05ea \u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd."
+ },
+ "step": {
+ "basic_options": {
+ "data": {
+ "known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd"
+ },
+ "description": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd - \u05e8\u05e9\u05d9\u05de\u05d4 \u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd \u05e9\u05dc \u05e9\u05de\u05d5\u05ea \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05d5\u05ea IP \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d9\u05e6\u05d5\u05e7\u05d9\u05dd, \u05d4\u05e9\u05ea\u05de\u05e9 \u05d0\u05dd \u05d2\u05d9\u05dc\u05d5\u05d9 mDNS \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc."
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/hu.json b/homeassistant/components/cast/translations/hu.json
index 660e598a7cb..3b5840b1c14 100644
--- a/homeassistant/components/cast/translations/hu.json
+++ b/homeassistant/components/cast/translations/hu.json
@@ -9,10 +9,10 @@
"step": {
"config": {
"data": {
- "known_hosts": "Opcion\u00e1lis lista az ismert hosztokr\u00f3l, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik."
+ "known_hosts": "Ismert hosztok"
},
"description": "K\u00e9rj\u00fck, add meg a Google Cast konfigur\u00e1ci\u00f3t.",
- "title": "Google Cast"
+ "title": "Google Cast konfigur\u00e1ci\u00f3"
},
"confirm": {
"description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?"
diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py
index f91eaab49b6..c4381b65c49 100644
--- a/homeassistant/components/cert_expiry/__init__.py
+++ b/homeassistant/components/cert_expiry/__init__.py
@@ -20,7 +20,7 @@ SCAN_INTERVAL = timedelta(hours=12)
PLATFORMS = ["sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Load the saved entities."""
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
diff --git a/homeassistant/components/cert_expiry/translations/he.json b/homeassistant/components/cert_expiry/translations/he.json
new file mode 100644
index 00000000000..9b55d58684e
--- /dev/null
+++ b/homeassistant/components/cert_expiry/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "error": {
+ "connection_refused": "\u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e0\u05d3\u05d7\u05d4 \u05d1\u05e2\u05ea \u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05de\u05d0\u05e8\u05d7",
+ "connection_timeout": "\u05d7\u05dc\u05e3 \u05d6\u05de\u05df \u05e7\u05e6\u05d5\u05d1 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05de\u05d0\u05e8\u05d7 \u05d6\u05d4",
+ "resolve_failed": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e4\u05e2\u05e0\u05d7 \u05de\u05d0\u05e8\u05d7 \u05d6\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py
index 85a23ef10a9..09909ae4e3a 100644
--- a/homeassistant/components/climacell/__init__.py
+++ b/homeassistant/components/climacell/__init__.py
@@ -109,19 +109,19 @@ def _set_update_interval(hass: HomeAssistant, current_entry: ConfigEntry) -> tim
return interval
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up ClimaCell API from a config entry."""
hass.data.setdefault(DOMAIN, {})
params = {}
# If config entry options not set up, set them up
- if not config_entry.options:
+ if not entry.options:
params["options"] = {
CONF_TIMESTEP: DEFAULT_TIMESTEP,
}
else:
# Use valid timestep if it's invalid
- timestep = config_entry.options[CONF_TIMESTEP]
+ timestep = entry.options[CONF_TIMESTEP]
if timestep not in (1, 5, 15, 30):
if timestep <= 2:
timestep = 1
@@ -131,38 +131,38 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
timestep = 15
else:
timestep = 30
- new_options = config_entry.options.copy()
+ new_options = entry.options.copy()
new_options[CONF_TIMESTEP] = timestep
params["options"] = new_options
# Add API version if not found
- if CONF_API_VERSION not in config_entry.data:
- new_data = config_entry.data.copy()
+ if CONF_API_VERSION not in entry.data:
+ new_data = entry.data.copy()
new_data[CONF_API_VERSION] = 3
params["data"] = new_data
if params:
- hass.config_entries.async_update_entry(config_entry, **params)
+ hass.config_entries.async_update_entry(entry, **params)
- api_class = ClimaCellV3 if config_entry.data[CONF_API_VERSION] == 3 else ClimaCellV4
+ api_class = ClimaCellV3 if entry.data[CONF_API_VERSION] == 3 else ClimaCellV4
api = api_class(
- config_entry.data[CONF_API_KEY],
- config_entry.data.get(CONF_LATITUDE, hass.config.latitude),
- config_entry.data.get(CONF_LONGITUDE, hass.config.longitude),
+ entry.data[CONF_API_KEY],
+ entry.data.get(CONF_LATITUDE, hass.config.latitude),
+ entry.data.get(CONF_LONGITUDE, hass.config.longitude),
session=async_get_clientsession(hass),
)
coordinator = ClimaCellDataUpdateCoordinator(
hass,
- config_entry,
+ entry,
api,
- _set_update_interval(hass, config_entry),
+ _set_update_interval(hass, entry),
)
await coordinator.async_config_entry_first_refresh()
- hass.data[DOMAIN][config_entry.entry_id] = coordinator
+ hass.data[DOMAIN][entry.entry_id] = coordinator
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py
index 0352807138a..062de93375b 100644
--- a/homeassistant/components/climacell/const.py
+++ b/homeassistant/components/climacell/const.py
@@ -5,6 +5,7 @@ from pyclimacell.const import (
NOWCAST,
HealthConcernType,
PollenIndex,
+ PrecipitationType,
PrimaryPollutantType,
V3PollenIndex,
WeatherCode,
@@ -26,13 +27,29 @@ from homeassistant.components.weather import (
)
from homeassistant.const import (
ATTR_NAME,
+ CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONF_UNIT_OF_MEASUREMENT,
CONF_UNIT_SYSTEM_IMPERIAL,
CONF_UNIT_SYSTEM_METRIC,
+ 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]
@@ -58,6 +75,7 @@ ATTR_FIELD = "field"
ATTR_METRIC_CONVERSION = "metric_conversion"
ATTR_VALUE_MAP = "value_map"
ATTR_IS_METRIC_CHECK = "is_metric_check"
+ATTR_SCALE = "scale"
# Additional attributes
ATTR_WIND_GUST = "wind_gust"
@@ -126,12 +144,103 @@ 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"
CC_SENSOR_TYPES = [
+ {
+ ATTR_FIELD: CC_ATTR_FEELS_LIKE,
+ ATTR_NAME: "Feels Like",
+ CONF_UNIT_SYSTEM_IMPERIAL: TEMP_FAHRENHEIT,
+ CONF_UNIT_SYSTEM_METRIC: TEMP_CELSIUS,
+ ATTR_METRIC_CONVERSION: lambda val: temp_convert(
+ val, TEMP_FAHRENHEIT, TEMP_CELSIUS
+ ),
+ ATTR_IS_METRIC_CHECK: True,
+ },
+ {
+ ATTR_FIELD: CC_ATTR_DEW_POINT,
+ ATTR_NAME: "Dew Point",
+ CONF_UNIT_SYSTEM_IMPERIAL: TEMP_FAHRENHEIT,
+ CONF_UNIT_SYSTEM_METRIC: TEMP_CELSIUS,
+ ATTR_METRIC_CONVERSION: lambda val: temp_convert(
+ val, TEMP_FAHRENHEIT, TEMP_CELSIUS
+ ),
+ ATTR_IS_METRIC_CHECK: True,
+ },
+ {
+ ATTR_FIELD: CC_ATTR_PRESSURE_SURFACE_LEVEL,
+ ATTR_NAME: "Pressure (Surface Level)",
+ CONF_UNIT_SYSTEM_IMPERIAL: PRESSURE_INHG,
+ CONF_UNIT_SYSTEM_METRIC: PRESSURE_HPA,
+ ATTR_METRIC_CONVERSION: lambda val: pressure_convert(
+ val, PRESSURE_INHG, PRESSURE_HPA
+ ),
+ ATTR_IS_METRIC_CHECK: True,
+ },
+ {
+ ATTR_FIELD: CC_ATTR_SOLAR_GHI,
+ ATTR_NAME: "Global Horizontal Irradiance",
+ CONF_UNIT_SYSTEM_IMPERIAL: IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT,
+ CONF_UNIT_SYSTEM_METRIC: IRRADIATION_WATTS_PER_SQUARE_METER,
+ ATTR_METRIC_CONVERSION: 3.15459,
+ ATTR_IS_METRIC_CHECK: True,
+ },
+ {
+ ATTR_FIELD: CC_ATTR_CLOUD_BASE,
+ ATTR_NAME: "Cloud Base",
+ CONF_UNIT_SYSTEM_IMPERIAL: LENGTH_MILES,
+ CONF_UNIT_SYSTEM_METRIC: LENGTH_KILOMETERS,
+ ATTR_METRIC_CONVERSION: lambda val: distance_convert(
+ val, LENGTH_MILES, LENGTH_KILOMETERS
+ ),
+ ATTR_IS_METRIC_CHECK: True,
+ },
+ {
+ ATTR_FIELD: CC_ATTR_CLOUD_CEILING,
+ ATTR_NAME: "Cloud Ceiling",
+ CONF_UNIT_SYSTEM_IMPERIAL: LENGTH_MILES,
+ CONF_UNIT_SYSTEM_METRIC: LENGTH_KILOMETERS,
+ ATTR_METRIC_CONVERSION: lambda val: distance_convert(
+ val, LENGTH_MILES, LENGTH_KILOMETERS
+ ),
+ ATTR_IS_METRIC_CHECK: True,
+ },
+ {
+ ATTR_FIELD: CC_ATTR_CLOUD_COVER,
+ ATTR_NAME: "Cloud Cover",
+ CONF_UNIT_OF_MEASUREMENT: PERCENTAGE,
+ ATTR_SCALE: 1 / 100,
+ },
+ {
+ ATTR_FIELD: CC_ATTR_WIND_GUST,
+ ATTR_NAME: "Wind Gust",
+ CONF_UNIT_SYSTEM_IMPERIAL: SPEED_MILES_PER_HOUR,
+ CONF_UNIT_SYSTEM_METRIC: SPEED_METERS_PER_SECOND,
+ ATTR_METRIC_CONVERSION: lambda val: distance_convert(
+ val, LENGTH_MILES, LENGTH_METERS
+ )
+ / 3600,
+ ATTR_IS_METRIC_CHECK: True,
+ },
+ {
+ ATTR_FIELD: CC_ATTR_PRECIPITATION_TYPE,
+ ATTR_NAME: "Precipitation Type",
+ ATTR_VALUE_MAP: PrecipitationType,
+ },
+ {
+ ATTR_FIELD: CC_ATTR_OZONE,
+ ATTR_NAME: "Ozone",
+ CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION,
+ },
{
ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_25,
ATTR_NAME: "Particulate Matter < 2.5 μm",
- CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³",
+ CONF_UNIT_SYSTEM_IMPERIAL: CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_METRIC_CONVERSION: 3.2808399 ** 3,
ATTR_IS_METRIC_CHECK: True,
@@ -139,7 +248,7 @@ CC_SENSOR_TYPES = [
{
ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_10,
ATTR_NAME: "Particulate Matter < 10 μm",
- CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³",
+ CONF_UNIT_SYSTEM_IMPERIAL: CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_METRIC_CONVERSION: 3.2808399 ** 3,
ATTR_IS_METRIC_CHECK: True,
@@ -262,12 +371,17 @@ CC_V3_ATTR_POLLEN_GRASS = "pollen_grass"
CC_V3_ATTR_FIRE_INDEX = "fire_index"
CC_V3_SENSOR_TYPES = [
+ {
+ ATTR_FIELD: CC_V3_ATTR_OZONE,
+ ATTR_NAME: "Ozone",
+ CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION,
+ },
{
ATTR_FIELD: CC_V3_ATTR_PARTICULATE_MATTER_25,
ATTR_NAME: "Particulate Matter < 2.5 μm",
CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³",
CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
- ATTR_METRIC_CONVERSION: 1 / (3.2808399 ** 3),
+ ATTR_METRIC_CONVERSION: 3.2808399 ** 3,
ATTR_IS_METRIC_CHECK: False,
},
{
@@ -275,7 +389,7 @@ CC_V3_SENSOR_TYPES = [
ATTR_NAME: "Particulate Matter < 10 μm",
CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³",
CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
- ATTR_METRIC_CONVERSION: 1 / (3.2808399 ** 3),
+ ATTR_METRIC_CONVERSION: 3.2808399 ** 3,
ATTR_IS_METRIC_CHECK: False,
},
{
diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py
index df611079403..2c620cc65a1 100644
--- a/homeassistant/components/climacell/sensor.py
+++ b/homeassistant/components/climacell/sensor.py
@@ -28,6 +28,7 @@ from .const import (
ATTR_FIELD,
ATTR_IS_METRIC_CHECK,
ATTR_METRIC_CONVERSION,
+ ATTR_SCALE,
ATTR_VALUE_MAP,
CC_SENSOR_TYPES,
CC_V3_SENSOR_TYPES,
@@ -103,9 +104,11 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity):
CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type
and CONF_UNIT_SYSTEM_METRIC in self.sensor_type
):
- if self.hass.config.units.is_metric:
- return self.sensor_type[CONF_UNIT_SYSTEM_METRIC]
- return self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL]
+ return (
+ self.sensor_type[CONF_UNIT_SYSTEM_METRIC]
+ if self.hass.config.units.is_metric
+ else self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL]
+ )
return None
@@ -117,8 +120,12 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity):
@property
def state(self) -> str | int | float | None:
"""Return the state."""
+ state = self._state
+ if state and ATTR_SCALE in self.sensor_type:
+ state *= self.sensor_type[ATTR_SCALE]
+
if (
- self._state is not None
+ state is not None
and CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type
and CONF_UNIT_SYSTEM_METRIC in self.sensor_type
and ATTR_METRIC_CONVERSION in self.sensor_type
@@ -126,11 +133,17 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity):
and self.hass.config.units.is_metric
== self.sensor_type[ATTR_IS_METRIC_CHECK]
):
- return round(self._state * self.sensor_type[ATTR_METRIC_CONVERSION], 4)
+ conversion = self.sensor_type[ATTR_METRIC_CONVERSION]
+ # When conversion is a callable, we assume it's a single input function
+ if callable(conversion):
+ return round(conversion(state), 4)
- if ATTR_VALUE_MAP in self.sensor_type and self._state is not None:
- return self.sensor_type[ATTR_VALUE_MAP](self._state).name.lower()
- return self._state
+ return round(state * conversion, 4)
+
+ if ATTR_VALUE_MAP in self.sensor_type and state is not None:
+ return self.sensor_type[ATTR_VALUE_MAP](state).name.lower()
+
+ return state
class ClimaCellSensorEntity(BaseClimaCellSensorEntity):
diff --git a/homeassistant/components/climacell/translations/he.json b/homeassistant/components/climacell/translations/he.json
index 81a4b5c1fce..b663a5e0a0f 100644
--- a/homeassistant/components/climacell/translations/he.json
+++ b/homeassistant/components/climacell/translations/he.json
@@ -1,12 +1,15 @@
{
"config": {
"error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
},
"step": {
"user": {
"data": {
"api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "api_version": "\u05d2\u05e8\u05e1\u05ea API",
"latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
"longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
"name": "\u05e9\u05dd"
diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py
index 30842f1fe23..dbd74d1c5e8 100644
--- a/homeassistant/components/climate/__init__.py
+++ b/homeassistant/components/climate/__init__.py
@@ -1,7 +1,6 @@
"""Provides functionality to interact with climate devices."""
from __future__ import annotations
-from abc import abstractmethod
from datetime import timedelta
import functools as ft
import logging
@@ -9,6 +8,7 @@ from typing import Any, final
import voluptuous as vol
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TEMPERATURE,
PRECISION_TENTHS,
@@ -157,19 +157,46 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
-async def async_setup_entry(hass: HomeAssistant, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass: HomeAssistant, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class ClimateEntity(Entity):
"""Base class for climate entities."""
+ _attr_current_humidity: int | None = None
+ _attr_current_temperature: float | None = None
+ _attr_fan_mode: str | None
+ _attr_fan_modes: list[str] | None
+ _attr_hvac_action: str | None = None
+ _attr_hvac_mode: str
+ _attr_hvac_modes: list[str]
+ _attr_is_aux_heat: bool | None
+ _attr_max_humidity: int = DEFAULT_MAX_HUMIDITY
+ _attr_max_temp: float
+ _attr_min_humidity: int = DEFAULT_MIN_HUMIDITY
+ _attr_min_temp: float
+ _attr_precision: float
+ _attr_preset_mode: str | None
+ _attr_preset_modes: list[str] | None
+ _attr_supported_features: int
+ _attr_swing_mode: str | None
+ _attr_swing_modes: list[str] | None
+ _attr_target_humidity: int | None = None
+ _attr_target_temperature_high: float | None
+ _attr_target_temperature_low: float | None
+ _attr_target_temperature_step: float | None = None
+ _attr_target_temperature: float | None = None
+ _attr_temperature_unit: str
+
@property
def state(self) -> str:
"""Return the current state."""
@@ -178,6 +205,8 @@ class ClimateEntity(Entity):
@property
def precision(self) -> float:
"""Return the precision of the system."""
+ if hasattr(self, "_attr_precision"):
+ return self._attr_precision
if self.hass.config.units.temperature_unit == TEMP_CELSIUS:
return PRECISION_TENTHS
return PRECISION_WHOLE
@@ -276,33 +305,33 @@ class ClimateEntity(Entity):
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement used by the platform."""
- raise NotImplementedError()
+ return self._attr_temperature_unit
@property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
- return None
+ return self._attr_current_humidity
@property
def target_humidity(self) -> int | None:
"""Return the humidity we try to reach."""
- return None
+ return self._attr_target_humidity
@property
- @abstractmethod
def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode.
Need to be one of HVAC_MODE_*.
"""
+ return self._attr_hvac_mode
@property
- @abstractmethod
def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes.
Need to be a subset of HVAC_MODES.
"""
+ return self._attr_hvac_modes
@property
def hvac_action(self) -> str | None:
@@ -310,22 +339,22 @@ class ClimateEntity(Entity):
Need to be one of CURRENT_HVAC_*.
"""
- return None
+ return self._attr_hvac_action
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
- return None
+ return self._attr_current_temperature
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
- return None
+ return self._attr_target_temperature
@property
def target_temperature_step(self) -> float | None:
"""Return the supported step of target temperature."""
- return None
+ return self._attr_target_temperature_step
@property
def target_temperature_high(self) -> float | None:
@@ -333,7 +362,7 @@ class ClimateEntity(Entity):
Requires SUPPORT_TARGET_TEMPERATURE_RANGE.
"""
- raise NotImplementedError
+ return self._attr_target_temperature_high
@property
def target_temperature_low(self) -> float | None:
@@ -341,7 +370,7 @@ class ClimateEntity(Entity):
Requires SUPPORT_TARGET_TEMPERATURE_RANGE.
"""
- raise NotImplementedError
+ return self._attr_target_temperature_low
@property
def preset_mode(self) -> str | None:
@@ -349,7 +378,7 @@ class ClimateEntity(Entity):
Requires SUPPORT_PRESET_MODE.
"""
- raise NotImplementedError
+ return self._attr_preset_mode
@property
def preset_modes(self) -> list[str] | None:
@@ -357,7 +386,7 @@ class ClimateEntity(Entity):
Requires SUPPORT_PRESET_MODE.
"""
- raise NotImplementedError
+ return self._attr_preset_modes
@property
def is_aux_heat(self) -> bool | None:
@@ -365,7 +394,7 @@ class ClimateEntity(Entity):
Requires SUPPORT_AUX_HEAT.
"""
- raise NotImplementedError
+ return self._attr_is_aux_heat
@property
def fan_mode(self) -> str | None:
@@ -373,7 +402,7 @@ class ClimateEntity(Entity):
Requires SUPPORT_FAN_MODE.
"""
- raise NotImplementedError
+ return self._attr_fan_mode
@property
def fan_modes(self) -> list[str] | None:
@@ -381,7 +410,7 @@ class ClimateEntity(Entity):
Requires SUPPORT_FAN_MODE.
"""
- raise NotImplementedError
+ return self._attr_fan_modes
@property
def swing_mode(self) -> str | None:
@@ -389,7 +418,7 @@ class ClimateEntity(Entity):
Requires SUPPORT_SWING_MODE.
"""
- raise NotImplementedError
+ return self._attr_swing_mode
@property
def swing_modes(self) -> list[str] | None:
@@ -397,7 +426,7 @@ class ClimateEntity(Entity):
Requires SUPPORT_SWING_MODE.
"""
- raise NotImplementedError
+ return self._attr_swing_modes
def set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
@@ -493,31 +522,35 @@ class ClimateEntity(Entity):
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
- raise NotImplementedError()
+ return self._attr_supported_features
@property
def min_temp(self) -> float:
"""Return the minimum temperature."""
- return convert_temperature(
- DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit
- )
+ if not hasattr(self, "_attr_min_temp"):
+ return convert_temperature(
+ DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit
+ )
+ return self._attr_min_temp
@property
def max_temp(self) -> float:
"""Return the maximum temperature."""
- return convert_temperature(
- DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit
- )
+ if not hasattr(self, "_attr_max_temp"):
+ return convert_temperature(
+ DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit
+ )
+ return self._attr_max_temp
@property
def min_humidity(self) -> int:
"""Return the minimum humidity."""
- return DEFAULT_MIN_HUMIDITY
+ return self._attr_min_humidity
@property
def max_humidity(self) -> int:
"""Return the maximum humidity."""
- return DEFAULT_MAX_HUMIDITY
+ return self._attr_max_humidity
async def async_service_aux_heat(
diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py
index af6c9364b18..55387d71438 100644
--- a/homeassistant/components/climate/const.py
+++ b/homeassistant/components/climate/const.py
@@ -63,6 +63,7 @@ FAN_AUTO = "auto"
FAN_LOW = "low"
FAN_MEDIUM = "medium"
FAN_HIGH = "high"
+FAN_TOP = "top"
FAN_MIDDLE = "middle"
FAN_FOCUS = "focus"
FAN_DIFFUSE = "diffuse"
diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py
index 02474a47f96..6afc4d294cb 100644
--- a/homeassistant/components/climate/device_action.py
+++ b/homeassistant/components/climate/device_action.py
@@ -5,15 +5,15 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
- ATTR_SUPPORTED_FEATURES,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_TYPE,
)
-from homeassistant.core import Context, HomeAssistant
+from homeassistant.core import Context, HomeAssistant, HomeAssistantError
from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import get_capability, get_supported_features
from . import DOMAIN, const
@@ -48,29 +48,17 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
if entry.domain != DOMAIN:
continue
- state = hass.states.get(entry.entity_id)
+ supported_features = get_supported_features(hass, entry.entity_id)
- # We need a state or else we can't populate the HVAC and preset modes.
- if state is None:
- continue
+ base_action = {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "set_hvac_mode",
- }
- )
- if state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_PRESET_MODE:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "set_preset_mode",
- }
- )
+ actions.append({**base_action, CONF_TYPE: "set_hvac_mode"})
+ if supported_features & const.SUPPORT_PRESET_MODE:
+ actions.append({**base_action, CONF_TYPE: "set_preset_mode"})
return actions
@@ -95,18 +83,26 @@ async def async_call_action_from_config(
async def async_get_action_capabilities(hass, config):
"""List action capabilities."""
- state = hass.states.get(config[CONF_ENTITY_ID])
action_type = config[CONF_TYPE]
fields = {}
if action_type == "set_hvac_mode":
- hvac_modes = state.attributes[const.ATTR_HVAC_MODES] if state else []
+ try:
+ hvac_modes = (
+ get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_HVAC_MODES)
+ or []
+ )
+ except HomeAssistantError:
+ hvac_modes = []
fields[vol.Required(const.ATTR_HVAC_MODE)] = vol.In(hvac_modes)
elif action_type == "set_preset_mode":
- if state:
- preset_modes = state.attributes.get(const.ATTR_PRESET_MODES, [])
- else:
+ try:
+ preset_modes = (
+ get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_PRESET_MODES)
+ or []
+ )
+ except HomeAssistantError:
preset_modes = []
fields[vol.Required(const.ATTR_PRESET_MODE)] = vol.In(preset_modes)
diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py
index d20c202e93b..3fc6d94ba4f 100644
--- a/homeassistant/components/climate/device_condition.py
+++ b/homeassistant/components/climate/device_condition.py
@@ -5,16 +5,16 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
- ATTR_SUPPORTED_FEATURES,
CONF_CONDITION,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_TYPE,
)
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant, HomeAssistantError, callback
from homeassistant.helpers import condition, config_validation as cv, entity_registry
from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
+from homeassistant.helpers.entity import get_capability, get_supported_features
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN, const
@@ -52,31 +52,19 @@ async def async_get_conditions(
if entry.domain != DOMAIN:
continue
- state = hass.states.get(entry.entity_id)
+ supported_features = get_supported_features(hass, entry.entity_id)
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_hvac_mode",
- }
- )
+ base_condition = {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
- if (
- state
- and state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_PRESET_MODE
- ):
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_preset_mode",
- }
- )
+ conditions.append({**base_condition, CONF_TYPE: "is_hvac_mode"})
+
+ if supported_features & const.SUPPORT_PRESET_MODE:
+ conditions.append({**base_condition, CONF_TYPE: "is_preset_mode"})
return conditions
@@ -104,21 +92,28 @@ def async_condition_from_config(
async def async_get_condition_capabilities(hass, config):
"""List condition capabilities."""
- state = hass.states.get(config[CONF_ENTITY_ID])
condition_type = config[CONF_TYPE]
fields = {}
if condition_type == "is_hvac_mode":
- hvac_modes = state.attributes[const.ATTR_HVAC_MODES] if state else []
+ try:
+ hvac_modes = (
+ get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_HVAC_MODES)
+ or []
+ )
+ except HomeAssistantError:
+ hvac_modes = []
fields[vol.Required(const.ATTR_HVAC_MODE)] = vol.In(hvac_modes)
elif condition_type == "is_preset_mode":
- if state:
- preset_modes = state.attributes.get(const.ATTR_PRESET_MODES, [])
- else:
+ try:
+ preset_modes = (
+ get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_PRESET_MODES)
+ or []
+ )
+ except HomeAssistantError:
preset_modes = []
-
fields[vol.Required(const.ATTR_PRESET_MODE)] = vol.In(preset_modes)
return {"extra_fields": vol.Schema(fields)}
diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py
index df925463d4c..1b5127d7d4a 100644
--- a/homeassistant/components/climate/device_trigger.py
+++ b/homeassistant/components/climate/device_trigger.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import (
numeric_state as numeric_state_trigger,
state as state_trigger,
@@ -32,7 +32,7 @@ TRIGGER_TYPES = {
"hvac_mode_changed",
}
-HVAC_MODE_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+HVAC_MODE_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): "hvac_mode_changed",
@@ -41,7 +41,7 @@ HVAC_MODE_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
)
CURRENT_TRIGGER_SCHEMA = vol.All(
- TRIGGER_BASE_SCHEMA.extend(
+ DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(
diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json
index d0d7ae09505..7516f32c3e1 100644
--- a/homeassistant/components/cloud/manifest.json
+++ b/homeassistant/components/cloud/manifest.json
@@ -2,7 +2,7 @@
"domain": "cloud",
"name": "Home Assistant Cloud",
"documentation": "https://www.home-assistant.io/integrations/cloud",
- "requirements": ["hass-nabucasa==0.43.0"],
+ "requirements": ["hass-nabucasa==0.44.0"],
"dependencies": ["http", "webhook"],
"after_dependencies": ["google_assistant", "alexa"],
"codeowners": ["@home-assistant/cloud"],
diff --git a/homeassistant/components/cloud/translations/he.json b/homeassistant/components/cloud/translations/he.json
new file mode 100644
index 00000000000..9ea65e73c4e
--- /dev/null
+++ b/homeassistant/components/cloud/translations/he.json
@@ -0,0 +1,9 @@
+{
+ "system_health": {
+ "info": {
+ "alexa_enabled": "Alexa \u05de\u05d5\u05e4\u05e2\u05dc\u05ea",
+ "google_enabled": "Google \u05de\u05d5\u05e4\u05e2\u05dc",
+ "logged_in": "\u05de\u05d7\u05d5\u05d1\u05e8"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py
index e461e34c9a2..2d0d3145ead 100644
--- a/homeassistant/components/cloudflare/__init__.py
+++ b/homeassistant/components/cloudflare/__init__.py
@@ -14,18 +14,12 @@ from pycfdns.exceptions import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_TOKEN, CONF_ZONE
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
+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.event import async_track_time_interval
-from .const import (
- CONF_RECORDS,
- DATA_UNDO_UPDATE_INTERVAL,
- DEFAULT_UPDATE_INTERVAL,
- DOMAIN,
- SERVICE_UPDATE_RECORDS,
-)
+from .const import CONF_RECORDS, DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_UPDATE_RECORDS
_LOGGER = logging.getLogger(__name__)
@@ -43,9 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
zone_id = await cfupdate.get_zone_id()
- except CloudflareAuthenticationException:
- _LOGGER.error("API access forbidden. Please reauthenticate")
- return False
+ except CloudflareAuthenticationException as error:
+ raise ConfigEntryAuthFailed from error
except CloudflareConnectionException as error:
raise ConfigEntryNotReady from error
@@ -64,12 +57,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error("Error updating zone %s: %s", entry.data[CONF_ZONE], error)
update_interval = timedelta(minutes=DEFAULT_UPDATE_INTERVAL)
- undo_interval = async_track_time_interval(hass, update_records, update_interval)
+ entry.async_on_unload(
+ async_track_time_interval(hass, update_records, update_interval)
+ )
hass.data.setdefault(DOMAIN, {})
- hass.data[DOMAIN][entry.entry_id] = {
- DATA_UNDO_UPDATE_INTERVAL: undo_interval,
- }
+ hass.data[DOMAIN][entry.entry_id] = {}
hass.services.async_register(DOMAIN, SERVICE_UPDATE_RECORDS, update_records_service)
@@ -78,7 +71,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Cloudflare config entry."""
- hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_INTERVAL]()
hass.data[DOMAIN].pop(entry.entry_id)
return True
diff --git a/homeassistant/components/cloudflare/config_flow.py b/homeassistant/components/cloudflare/config_flow.py
index 364700427da..2a369fe65e0 100644
--- a/homeassistant/components/cloudflare/config_flow.py
+++ b/homeassistant/components/cloudflare/config_flow.py
@@ -2,6 +2,7 @@
from __future__ import annotations
import logging
+from typing import Any
from pycfdns import CloudflareUpdater
from pycfdns.exceptions import (
@@ -12,9 +13,10 @@ from pycfdns.exceptions import (
import voluptuous as vol
from homeassistant.components import persistent_notification
-from homeassistant.config_entries import ConfigFlow
+from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import CONF_API_TOKEN, CONF_ZONE
from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -85,12 +87,49 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ entry: ConfigEntry | None = None
+
def __init__(self):
"""Initialize the Cloudflare config flow."""
self.cloudflare_config = {}
self.zones = None
self.records = None
+ async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult:
+ """Handle initiation of re-authentication with Cloudflare."""
+ 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:
+ """Handle re-authentication with Cloudflare."""
+ errors = {}
+
+ if user_input is not None and self.entry:
+ _, errors = await self._async_validate_or_error(user_input)
+
+ if not errors:
+ self.hass.config_entries.async_update_entry(
+ self.entry,
+ data={
+ **self.entry.data,
+ CONF_API_TOKEN: user_input[CONF_API_TOKEN],
+ },
+ )
+
+ self.hass.async_create_task(
+ 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,
+ errors=errors,
+ )
+
async def async_step_user(self, user_input: dict | None = None):
"""Handle a flow initiated by the user."""
if self._async_current_entries():
diff --git a/homeassistant/components/cloudflare/const.py b/homeassistant/components/cloudflare/const.py
index 0bdce7b9a92..4952b3768b0 100644
--- a/homeassistant/components/cloudflare/const.py
+++ b/homeassistant/components/cloudflare/const.py
@@ -5,9 +5,6 @@ DOMAIN = "cloudflare"
# Config
CONF_RECORDS = "records"
-# Data
-DATA_UNDO_UPDATE_INTERVAL = "undo_update_interval"
-
# Defaults
DEFAULT_UPDATE_INTERVAL = 60 # in minutes
diff --git a/homeassistant/components/cloudflare/strings.json b/homeassistant/components/cloudflare/strings.json
index bdadfde4800..31df9a62341 100644
--- a/homeassistant/components/cloudflare/strings.json
+++ b/homeassistant/components/cloudflare/strings.json
@@ -20,6 +20,12 @@
"data": {
"records": "Records"
}
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Re-authenticate with your Cloudflare account.",
+ "api_token": "[%key:common::config_flow::data::api_token%]"
+ }
}
},
"error": {
@@ -28,6 +34,7 @@
"invalid_zone": "Invalid zone"
},
"abort": {
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
diff --git a/homeassistant/components/cloudflare/translations/de.json b/homeassistant/components/cloudflare/translations/de.json
index 21118e106bf..d03f293b38b 100644
--- a/homeassistant/components/cloudflare/translations/de.json
+++ b/homeassistant/components/cloudflare/translations/de.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich",
"single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
"unknown": "Unerwarteter Fehler"
},
@@ -9,8 +10,14 @@
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"invalid_zone": "Ung\u00fcltige Zone"
},
- "flow_title": "Cloudflare: {name}",
+ "flow_title": "{name}",
"step": {
+ "reauth_confirm": {
+ "data": {
+ "api_token": "API-Token",
+ "description": "Authentifiziere dich erneut mit deinem Cloudflare-Konto."
+ }
+ },
"records": {
"data": {
"records": "Datens\u00e4tze"
diff --git a/homeassistant/components/cloudflare/translations/en.json b/homeassistant/components/cloudflare/translations/en.json
index 3dcd60ac4a9..43034cfbb4a 100644
--- a/homeassistant/components/cloudflare/translations/en.json
+++ b/homeassistant/components/cloudflare/translations/en.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "reauth_successful": "Re-authentication was successful",
"single_instance_allowed": "Already configured. Only a single configuration possible.",
"unknown": "Unexpected error"
},
@@ -11,6 +12,12 @@
},
"flow_title": "{name}",
"step": {
+ "reauth_confirm": {
+ "data": {
+ "api_token": "API Token",
+ "description": "Re-authenticate with your Cloudflare account."
+ }
+ },
"records": {
"data": {
"records": "Records"
diff --git a/homeassistant/components/cloudflare/translations/et.json b/homeassistant/components/cloudflare/translations/et.json
index 1688372fa1e..779706b29a4 100644
--- a/homeassistant/components/cloudflare/translations/et.json
+++ b/homeassistant/components/cloudflare/translations/et.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "reauth_successful": "Taastuvastamine \u00f5nnestus",
"single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.",
"unknown": "Tundmatu viga"
},
@@ -11,6 +12,12 @@
},
"flow_title": "{name}",
"step": {
+ "reauth_confirm": {
+ "data": {
+ "api_token": "API v\u00f5ti",
+ "description": "Taastuvasta oma Cloudflare'i kontoga."
+ }
+ },
"records": {
"data": {
"records": "Kirjed"
diff --git a/homeassistant/components/cloudflare/translations/he.json b/homeassistant/components/cloudflare/translations/he.json
new file mode 100644
index 00000000000..445cf45325d
--- /dev/null
+++ b/homeassistant/components/cloudflare/translations/he.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "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",
+ "invalid_zone": "\u05d0\u05d6\u05d5\u05e8 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "records": {
+ "data": {
+ "records": "\u05e8\u05e9\u05d5\u05de\u05d5\u05ea"
+ }
+ },
+ "user": {
+ "data": {
+ "api_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API"
+ }
+ },
+ "zone": {
+ "data": {
+ "zone": "\u05d0\u05b5\u05d6\u05d5\u05b9\u05e8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloudflare/translations/hu.json b/homeassistant/components/cloudflare/translations/hu.json
index fed6f22d536..a0f250376da 100644
--- a/homeassistant/components/cloudflare/translations/hu.json
+++ b/homeassistant/components/cloudflare/translations/hu.json
@@ -9,7 +9,7 @@
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"invalid_zone": "\u00c9rv\u00e9nytelen z\u00f3na"
},
- "flow_title": "Cloudflare: {name}",
+ "flow_title": "{name}",
"step": {
"records": {
"data": {
diff --git a/homeassistant/components/cloudflare/translations/nl.json b/homeassistant/components/cloudflare/translations/nl.json
index 7095ff49983..517743be9aa 100644
--- a/homeassistant/components/cloudflare/translations/nl.json
+++ b/homeassistant/components/cloudflare/translations/nl.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "reauth_successful": "Herauthenticatie was succesvol",
"single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.",
"unknown": "Onverwachte fout"
},
@@ -11,6 +12,12 @@
},
"flow_title": "{name}",
"step": {
+ "reauth_confirm": {
+ "data": {
+ "api_token": "API-token",
+ "description": "Verifieer opnieuw met uw Cloudflare-account."
+ }
+ },
"records": {
"data": {
"records": "Records"
diff --git a/homeassistant/components/cloudflare/translations/no.json b/homeassistant/components/cloudflare/translations/no.json
index 4c99154a4e3..1329429474a 100644
--- a/homeassistant/components/cloudflare/translations/no.json
+++ b/homeassistant/components/cloudflare/translations/no.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket",
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.",
"unknown": "Uventet feil"
},
@@ -11,6 +12,12 @@
},
"flow_title": "{name}",
"step": {
+ "reauth_confirm": {
+ "data": {
+ "api_token": "API-token",
+ "description": "Autentiser p\u00e5 nytt med Cloudflare-kontoen din."
+ }
+ },
"records": {
"data": {
"records": "Poster"
diff --git a/homeassistant/components/cloudflare/translations/ru.json b/homeassistant/components/cloudflare/translations/ru.json
index 2b8eb0b140a..5b4eb7e67a3 100644
--- a/homeassistant/components/cloudflare/translations/ru.json
+++ b/homeassistant/components/cloudflare/translations/ru.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.",
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
@@ -11,6 +12,12 @@
},
"flow_title": "{name}",
"step": {
+ "reauth_confirm": {
+ "data": {
+ "api_token": "\u0422\u043e\u043a\u0435\u043d API",
+ "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 Cloudflare."
+ }
+ },
"records": {
"data": {
"records": "\u0417\u0430\u043f\u0438\u0441\u0438"
diff --git a/homeassistant/components/cloudflare/translations/zh-Hant.json b/homeassistant/components/cloudflare/translations/zh-Hant.json
index da11b44ea8e..3ee29277296 100644
--- a/homeassistant/components/cloudflare/translations/zh-Hant.json
+++ b/homeassistant/components/cloudflare/translations/zh-Hant.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f",
"single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
@@ -11,6 +12,12 @@
},
"flow_title": "{name}",
"step": {
+ "reauth_confirm": {
+ "data": {
+ "api_token": "API \u6b0a\u6756",
+ "description": "\u91cd\u65b0\u8a8d\u8b49 Cloudflare \u5e33\u865f\u3002"
+ }
+ },
"records": {
"data": {
"records": "\u8a18\u9304"
diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py
index c7d2a64d6b0..980ffa8549b 100644
--- a/homeassistant/components/co2signal/sensor.py
+++ b/homeassistant/components/co2signal/sensor.py
@@ -54,6 +54,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class CO2Sensor(SensorEntity):
"""Implementation of the CO2Signal sensor."""
+ _attr_icon = "mdi:molecule-co2"
+ _attr_unit_of_measurement = CO2_INTENSITY_UNIT
+
def __init__(self, token, country_code, lat, lon):
"""Initialize the sensor."""
self._token = token
@@ -74,21 +77,11 @@ class CO2Sensor(SensorEntity):
"""Return the name of the sensor."""
return self._friendly_name
- @property
- def icon(self):
- """Icon to use in the frontend, if any."""
- return "mdi:molecule-co2"
-
@property
def state(self):
"""Return the state of the device."""
return self._data
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return CO2_INTENSITY_UNIT
-
@property
def extra_state_attributes(self):
"""Return the state attributes of the last update."""
diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py
index 5bcd330c9bb..08b97756dff 100644
--- a/homeassistant/components/coinbase/__init__.py
+++ b/homeassistant/components/coinbase/__init__.py
@@ -1,4 +1,6 @@
-"""Support for Coinbase."""
+"""The Coinbase integration."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
@@ -6,105 +8,151 @@ from coinbase.wallet.client import Client
from coinbase.wallet.error import AuthenticationError
import voluptuous as vol
-from homeassistant.const import CONF_API_KEY
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.discovery import load_platform
+from homeassistant.helpers.typing import ConfigType
from homeassistant.util import Throttle
+from .const import (
+ API_ACCOUNT_ID,
+ API_ACCOUNTS_DATA,
+ CONF_CURRENCIES,
+ CONF_EXCHANGE_RATES,
+ CONF_YAML_API_TOKEN,
+ DOMAIN,
+)
+
_LOGGER = logging.getLogger(__name__)
-DOMAIN = "coinbase"
-
-CONF_API_SECRET = "api_secret"
-CONF_ACCOUNT_CURRENCIES = "account_balance_currencies"
-CONF_EXCHANGE_CURRENCIES = "exchange_rate_currencies"
-
+PLATFORMS = ["sensor"]
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
-DATA_COINBASE = "coinbase_cache"
CONFIG_SCHEMA = vol.Schema(
+ cv.deprecated(DOMAIN),
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_API_KEY): cv.string,
- vol.Required(CONF_API_SECRET): cv.string,
- vol.Optional(CONF_ACCOUNT_CURRENCIES): vol.All(
+ vol.Required(CONF_YAML_API_TOKEN): cv.string,
+ vol.Optional(CONF_CURRENCIES): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(CONF_EXCHANGE_RATES, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
- vol.Optional(CONF_EXCHANGE_CURRENCIES, default=[]): vol.All(
- cv.ensure_list, [cv.string]
- ),
- }
+ },
)
},
extra=vol.ALLOW_EXTRA,
)
-def setup(hass, config):
- """Set up the Coinbase component.
-
- Will automatically setup sensors to support
- wallets discovered on the network.
- """
- api_key = config[DOMAIN][CONF_API_KEY]
- api_secret = config[DOMAIN][CONF_API_SECRET]
- account_currencies = config[DOMAIN].get(CONF_ACCOUNT_CURRENCIES)
- exchange_currencies = config[DOMAIN][CONF_EXCHANGE_CURRENCIES]
-
- hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(api_key, api_secret)
-
- if not hasattr(coinbase_data, "accounts"):
- return False
- for account in coinbase_data.accounts:
- if account_currencies is None or account.currency in account_currencies:
- load_platform(hass, "sensor", DOMAIN, {"account": account}, config)
- for currency in exchange_currencies:
- if currency not in coinbase_data.exchange_rates.rates:
- _LOGGER.warning("Currency %s not found", currency)
- continue
- native = coinbase_data.exchange_rates.currency
- load_platform(
- hass,
- "sensor",
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+ """Set up the Coinbase component."""
+ if DOMAIN not in config:
+ return True
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
DOMAIN,
- {"native_currency": native, "exchange_currency": currency},
- config,
+ context={"source": SOURCE_IMPORT},
+ data=config[DOMAIN],
)
+ )
return True
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up Coinbase from a config entry."""
+
+ instance = await hass.async_add_executor_job(
+ create_and_update_instance, entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN]
+ )
+
+ entry.async_on_unload(entry.add_update_listener(update_listener))
+
+ hass.data.setdefault(DOMAIN, {})
+
+ hass.data[DOMAIN][entry.entry_id] = instance
+
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+def create_and_update_instance(api_key, api_token):
+ """Create and update a Coinbase Data instance."""
+ client = Client(api_key, api_token)
+ instance = CoinbaseData(client)
+ instance.update()
+ return instance
+
+
+async def update_listener(hass, config_entry):
+ """Handle options update."""
+
+ await hass.config_entries.async_reload(config_entry.entry_id)
+
+ registry = entity_registry.async_get(hass)
+ entities = entity_registry.async_entries_for_config_entry(
+ registry, config_entry.entry_id
+ )
+
+ # Remove orphaned entities
+ for entity in entities:
+ currency = entity.unique_id.split("-")[-1]
+ if "xe" in entity.unique_id and currency not in config_entry.options.get(
+ CONF_EXCHANGE_RATES
+ ):
+ registry.async_remove(entity.entity_id)
+ elif "wallet" in entity.unique_id and currency not in config_entry.options.get(
+ CONF_CURRENCIES
+ ):
+ registry.async_remove(entity.entity_id)
+
+
+def get_accounts(client):
+ """Handle paginated accounts."""
+ response = client.get_accounts()
+ accounts = response[API_ACCOUNTS_DATA]
+ next_starting_after = response.pagination.next_starting_after
+
+ while next_starting_after:
+ response = client.get_accounts(starting_after=next_starting_after)
+ accounts += response[API_ACCOUNTS_DATA]
+ next_starting_after = response.pagination.next_starting_after
+
+ return accounts
+
+
class CoinbaseData:
"""Get the latest data and update the states."""
- def __init__(self, api_key, api_secret):
+ def __init__(self, client):
"""Init the coinbase data object."""
- self.client = Client(api_key, api_secret)
- self.update()
+ self.client = client
+ self.accounts = None
+ self.exchange_rates = None
+ self.user_id = self.client.get_current_user()[API_ACCOUNT_ID]
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from coinbase."""
try:
- response = self.client.get_accounts()
- accounts = response["data"]
-
- # Most of Coinbase's API seems paginated now (25 items per page, but first page has 24).
- # This API gives a 'next_starting_after' property to send back as a 'starting_after' param.
- # Their API documentation is not up to date when writing these lines (2021-05-20)
- next_starting_after = response.pagination.next_starting_after
-
- while next_starting_after:
- response = self.client.get_accounts(starting_after=next_starting_after)
- accounts = accounts + response["data"]
- next_starting_after = response.pagination.next_starting_after
-
- self.accounts = accounts
-
+ self.accounts = get_accounts(self.client)
self.exchange_rates = self.client.get_exchange_rates()
except AuthenticationError as coinbase_error:
_LOGGER.error(
diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py
new file mode 100644
index 00000000000..adfa9977518
--- /dev/null
+++ b/homeassistant/components/coinbase/config_flow.py
@@ -0,0 +1,215 @@
+"""Config flow for Coinbase integration."""
+import logging
+
+from coinbase.wallet.client import Client
+from coinbase.wallet.error import AuthenticationError
+import voluptuous as vol
+
+from homeassistant import config_entries, core, exceptions
+from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
+from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
+
+from . import get_accounts
+from .const import (
+ API_ACCOUNT_CURRENCY,
+ API_RATES,
+ CONF_CURRENCIES,
+ CONF_EXCHANGE_RATES,
+ CONF_OPTIONS,
+ CONF_YAML_API_TOKEN,
+ DOMAIN,
+ RATES,
+ WALLETS,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+STEP_USER_DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_API_KEY): str,
+ vol.Required(CONF_API_TOKEN): str,
+ }
+)
+
+
+def get_user_from_client(api_key, api_token):
+ """Get the user name from Coinbase API credentials."""
+ client = Client(api_key, api_token)
+ user = client.get_current_user()
+ return user
+
+
+async def validate_api(hass: core.HomeAssistant, data):
+ """Validate the credentials."""
+
+ try:
+ user = await hass.async_add_executor_job(
+ get_user_from_client, data[CONF_API_KEY], data[CONF_API_TOKEN]
+ )
+ except AuthenticationError as error:
+ raise InvalidAuth from error
+ except ConnectionError as error:
+ raise CannotConnect from error
+
+ return {"title": user["name"]}
+
+
+async def validate_options(
+ hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, options
+):
+ """Validate the requested resources are provided by API."""
+
+ client = hass.data[DOMAIN][config_entry.entry_id].client
+
+ accounts = await hass.async_add_executor_job(get_accounts, client)
+
+ accounts_currencies = [account[API_ACCOUNT_CURRENCY] for account in accounts]
+ available_rates = await hass.async_add_executor_job(client.get_exchange_rates)
+ if CONF_CURRENCIES in options:
+ for currency in options[CONF_CURRENCIES]:
+ if currency not in accounts_currencies:
+ raise CurrencyUnavaliable
+
+ if CONF_EXCHANGE_RATES in options:
+ for rate in options[CONF_EXCHANGE_RATES]:
+ if rate not in available_rates[API_RATES]:
+ raise ExchangeRateUnavaliable
+
+ return True
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Coinbase."""
+
+ VERSION = 1
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+ if user_input is None:
+ return self.async_show_form(
+ step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
+ )
+
+ self._async_abort_entries_match({CONF_API_KEY: user_input[CONF_API_KEY]})
+
+ options = {}
+
+ if CONF_OPTIONS in user_input:
+ options = user_input.pop(CONF_OPTIONS)
+
+ try:
+ info = await validate_api(self.hass, user_input)
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+ except InvalidAuth:
+ errors["base"] = "invalid_auth"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+ return self.async_create_entry(
+ title=info["title"], data=user_input, options=options
+ )
+ return self.async_show_form(
+ step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
+ )
+
+ async def async_step_import(self, config):
+ """Handle import of Coinbase config from YAML."""
+
+ cleaned_data = {
+ CONF_API_KEY: config[CONF_API_KEY],
+ CONF_API_TOKEN: config[CONF_YAML_API_TOKEN],
+ }
+ cleaned_data[CONF_OPTIONS] = {
+ CONF_CURRENCIES: [],
+ CONF_EXCHANGE_RATES: [],
+ }
+ if CONF_CURRENCIES in config:
+ cleaned_data[CONF_OPTIONS][CONF_CURRENCIES] = config[CONF_CURRENCIES]
+ if CONF_EXCHANGE_RATES in config:
+ cleaned_data[CONF_OPTIONS][CONF_EXCHANGE_RATES] = config[
+ CONF_EXCHANGE_RATES
+ ]
+
+ return await self.async_step_user(user_input=cleaned_data)
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return OptionsFlowHandler(config_entry)
+
+
+class OptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle a option flow for Coinbase."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Manage the options."""
+
+ errors = {}
+ default_currencies = self.config_entry.options.get(CONF_CURRENCIES, [])
+ default_exchange_rates = self.config_entry.options.get(CONF_EXCHANGE_RATES, [])
+
+ if user_input is not None:
+ # Pass back user selected options, even if bad
+ if CONF_CURRENCIES in user_input:
+ default_currencies = user_input[CONF_CURRENCIES]
+
+ if CONF_EXCHANGE_RATES in user_input:
+ default_exchange_rates = user_input[CONF_EXCHANGE_RATES]
+
+ try:
+ await validate_options(self.hass, self.config_entry, user_input)
+ except CurrencyUnavaliable:
+ errors["base"] = "currency_unavaliable"
+ except ExchangeRateUnavaliable:
+ errors["base"] = "exchange_rate_unavaliable"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+ return self.async_create_entry(title="", data=user_input)
+
+ return self.async_show_form(
+ step_id="init",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_CURRENCIES,
+ default=default_currencies,
+ ): cv.multi_select(WALLETS),
+ vol.Optional(
+ CONF_EXCHANGE_RATES,
+ default=default_exchange_rates,
+ ): cv.multi_select(RATES),
+ }
+ ),
+ errors=errors,
+ )
+
+
+class CannotConnect(exceptions.HomeAssistantError):
+ """Error to indicate we cannot connect."""
+
+
+class InvalidAuth(exceptions.HomeAssistantError):
+ """Error to indicate there is invalid auth."""
+
+
+class AlreadyConfigured(exceptions.HomeAssistantError):
+ """Error to indicate Coinbase API Key is already configured."""
+
+
+class CurrencyUnavaliable(exceptions.HomeAssistantError):
+ """Error to indicate the requested currency resource is not provided by the API."""
+
+
+class ExchangeRateUnavaliable(exceptions.HomeAssistantError):
+ """Error to indicate the requested exchange rate resource is not provided by the API."""
diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py
new file mode 100644
index 00000000000..035706c46ce
--- /dev/null
+++ b/homeassistant/components/coinbase/const.py
@@ -0,0 +1,487 @@
+"""Constants used for Coinbase."""
+
+CONF_CURRENCIES = "account_balance_currencies"
+CONF_EXCHANGE_RATES = "exchange_rate_currencies"
+CONF_OPTIONS = "options"
+DOMAIN = "coinbase"
+
+# These are constants used by the previous YAML configuration
+CONF_YAML_API_TOKEN = "api_secret"
+
+# Constants for data returned by Coinbase API
+API_ACCOUNT_AMOUNT = "amount"
+API_ACCOUNT_BALANCE = "balance"
+API_ACCOUNT_CURRENCY = "currency"
+API_ACCOUNT_ID = "id"
+API_ACCOUNT_NATIVE_BALANCE = "native_balance"
+API_ACCOUNT_NAME = "name"
+API_ACCOUNTS_DATA = "data"
+API_RATES = "rates"
+
+WALLETS = {
+ "1INCH": "1INCH",
+ "AAVE": "AAVE",
+ "ADA": "ADA",
+ "AED": "AED",
+ "AFN": "AFN",
+ "ALGO": "ALGO",
+ "ALL": "ALL",
+ "AMD": "AMD",
+ "AMP": "AMP",
+ "ANG": "ANG",
+ "ANKR": "ANKR",
+ "AOA": "AOA",
+ "ARS": "ARS",
+ "ATOM": "ATOM",
+ "AUD": "AUD",
+ "AWG": "AWG",
+ "AZN": "AZN",
+ "BAL": "BAL",
+ "BAM": "BAM",
+ "BAND": "BAND",
+ "BAT": "BAT",
+ "BBD": "BBD",
+ "BCH": "BCH",
+ "BDT": "BDT",
+ "BGN": "BGN",
+ "BHD": "BHD",
+ "BIF": "BIF",
+ "BMD": "BMD",
+ "BND": "BND",
+ "BNT": "BNT",
+ "BOB": "BOB",
+ "BOND": "BOND",
+ "BRL": "BRL",
+ "BSD": "BSD",
+ "BSV": "BSV",
+ "BTC": "BTC",
+ "BTN": "BTN",
+ "BWP": "BWP",
+ "BYN": "BYN",
+ "BYR": "BYR",
+ "BZD": "BZD",
+ "CAD": "CAD",
+ "CDF": "CDF",
+ "CGLD": "CGLD",
+ "CHF": "CHF",
+ "CHZ": "CHZ",
+ "CLF": "CLF",
+ "CLP": "CLP",
+ "CNH": "CNH",
+ "CNY": "CNY",
+ "COMP": "COMP",
+ "COP": "COP",
+ "CRC": "CRC",
+ "CRV": "CRV",
+ "CTSI": "CTSI",
+ "CUC": "CUC",
+ "CVC": "CVC",
+ "CVE": "CVE",
+ "CZK": "CZK",
+ "DAI": "DAI",
+ "DASH": "DASH",
+ "DJF": "DJF",
+ "DKK": "DKK",
+ "DNT": "DNT",
+ "DOGE": "DOGE",
+ "DOP": "DOP",
+ "DOT": "DOT",
+ "DZD": "DZD",
+ "EGP": "EGP",
+ "ENJ": "ENJ",
+ "EOS": "EOS",
+ "ERN": "ERN",
+ "ETB": "ETB",
+ "ETC": "ETC",
+ "ETH": "ETH",
+ "ETH2": "ETH2",
+ "EUR": "EUR",
+ "FIL": "FIL",
+ "FJD": "FJD",
+ "FKP": "FKP",
+ "FORTH": "FORTH",
+ "GBP": "GBP",
+ "GBX": "GBX",
+ "GEL": "GEL",
+ "GGP": "GGP",
+ "GHS": "GHS",
+ "GIP": "GIP",
+ "GMD": "GMD",
+ "GNF": "GNF",
+ "GRT": "GRT",
+ "GTC": "GTC",
+ "GTQ": "GTQ",
+ "GYD": "GYD",
+ "HKD": "HKD",
+ "HNL": "HNL",
+ "HRK": "HRK",
+ "HTG": "HTG",
+ "HUF": "HUF",
+ "ICP": "ICP",
+ "IDR": "IDR",
+ "ILS": "ILS",
+ "IMP": "IMP",
+ "INR": "INR",
+ "IQD": "IQD",
+ "ISK": "ISK",
+ "JEP": "JEP",
+ "JMD": "JMD",
+ "JOD": "JOD",
+ "JPY": "JPY",
+ "KEEP": "KEEP",
+ "KES": "KES",
+ "KGS": "KGS",
+ "KHR": "KHR",
+ "KMF": "KMF",
+ "KNC": "KNC",
+ "KRW": "KRW",
+ "KWD": "KWD",
+ "KYD": "KYD",
+ "KZT": "KZT",
+ "LAK": "LAK",
+ "LBP": "LBP",
+ "LINK": "LINK",
+ "LKR": "LKR",
+ "LPT": "LPT",
+ "LRC": "LRC",
+ "LRD": "LRD",
+ "LSL": "LSL",
+ "LTC": "LTC",
+ "LYD": "LYD",
+ "MAD": "MAD",
+ "MANA": "MANA",
+ "MATIC": "MATIC",
+ "MDL": "MDL",
+ "MGA": "MGA",
+ "MIR": "MIR",
+ "MKD": "MKD",
+ "MKR": "MKR",
+ "MLN": "MLN",
+ "MMK": "MMK",
+ "MNT": "MNT",
+ "MOP": "MOP",
+ "MRO": "MRO",
+ "MTL": "MTL",
+ "MUR": "MUR",
+ "MVR": "MVR",
+ "MWK": "MWK",
+ "MXN": "MXN",
+ "MYR": "MYR",
+ "MZN": "MZN",
+ "NAD": "NAD",
+ "NGN": "NGN",
+ "NIO": "NIO",
+ "NKN": "NKN",
+ "NMR": "NMR",
+ "NOK": "NOK",
+ "NPR": "NPR",
+ "NU": "NU",
+ "NZD": "NZD",
+ "OGN": "OGN",
+ "OMG": "OMG",
+ "OMR": "OMR",
+ "OXT": "OXT",
+ "PAB": "PAB",
+ "PEN": "PEN",
+ "PGK": "PGK",
+ "PHP": "PHP",
+ "PKR": "PKR",
+ "PLN": "PLN",
+ "PYG": "PYG",
+ "QAR": "QAR",
+ "QNT": "QNT",
+ "REN": "REN",
+ "REP": "REP",
+ "REPV2": "REPV2",
+ "RLC": "RLC",
+ "RON": "RON",
+ "RSD": "RSD",
+ "RUB": "RUB",
+ "RWF": "RWF",
+ "SAR": "SAR",
+ "SBD": "SBD",
+ "SCR": "SCR",
+ "SEK": "SEK",
+ "SGD": "SGD",
+ "SHP": "SHP",
+ "SKL": "SKL",
+ "SLL": "SLL",
+ "SNX": "SNX",
+ "SOL": "SOL",
+ "SOS": "SOS",
+ "SRD": "SRD",
+ "SSP": "SSP",
+ "STD": "STD",
+ "STORJ": "STORJ",
+ "SUSHI": "SUSHI",
+ "SVC": "SVC",
+ "SZL": "SZL",
+ "THB": "THB",
+ "TJS": "TJS",
+ "TMM": "TMM",
+ "TMT": "TMT",
+ "TND": "TND",
+ "TOP": "TOP",
+ "TRB": "TRB",
+ "TRY": "TRY",
+ "TTD": "TTD",
+ "TWD": "TWD",
+ "TZS": "TZS",
+ "UAH": "UAH",
+ "UGX": "UGX",
+ "UMA": "UMA",
+ "UNI": "UNI",
+ "USD": "USD",
+ "USDC": "USDC",
+ "USDT": "USDT",
+ "UYU": "UYU",
+ "UZS": "UZS",
+ "VES": "VES",
+ "VND": "VND",
+ "VUV": "VUV",
+ "WBTC": "WBTC",
+ "WST": "WST",
+ "XAF": "XAF",
+ "XAG": "XAG",
+ "XAU": "XAU",
+ "XCD": "XCD",
+ "XDR": "XDR",
+ "XLM": "XLM",
+ "XOF": "XOF",
+ "XPD": "XPD",
+ "XPF": "XPF",
+ "XPT": "XPT",
+ "XRP": "XRP",
+ "XTZ": "XTZ",
+ "YER": "YER",
+ "YFI": "YFI",
+ "ZAR": "ZAR",
+ "ZEC": "ZEC",
+ "ZMW": "ZMW",
+ "ZRX": "ZRX",
+ "ZWL": "ZWL",
+}
+
+RATES = {
+ "1INCH": "1INCH",
+ "AAVE": "AAVE",
+ "ADA": "ADA",
+ "AED": "AED",
+ "AFN": "AFN",
+ "ALGO": "ALGO",
+ "ALL": "ALL",
+ "AMD": "AMD",
+ "ANG": "ANG",
+ "ANKR": "ANKR",
+ "AOA": "AOA",
+ "ARS": "ARS",
+ "ATOM": "ATOM",
+ "AUD": "AUD",
+ "AWG": "AWG",
+ "AZN": "AZN",
+ "BAL": "BAL",
+ "BAM": "BAM",
+ "BAND": "BAND",
+ "BAT": "BAT",
+ "BBD": "BBD",
+ "BCH": "BCH",
+ "BDT": "BDT",
+ "BGN": "BGN",
+ "BHD": "BHD",
+ "BIF": "BIF",
+ "BMD": "BMD",
+ "BND": "BND",
+ "BNT": "BNT",
+ "BOB": "BOB",
+ "BRL": "BRL",
+ "BSD": "BSD",
+ "BSV": "BSV",
+ "BTC": "BTC",
+ "BTN": "BTN",
+ "BWP": "BWP",
+ "BYN": "BYN",
+ "BYR": "BYR",
+ "BZD": "BZD",
+ "CAD": "CAD",
+ "CDF": "CDF",
+ "CGLD": "CGLD",
+ "CHF": "CHF",
+ "CLF": "CLF",
+ "CLP": "CLP",
+ "CNH": "CNH",
+ "CNY": "CNY",
+ "COMP": "COMP",
+ "COP": "COP",
+ "CRC": "CRC",
+ "CRV": "CRV",
+ "CUC": "CUC",
+ "CVC": "CVC",
+ "CVE": "CVE",
+ "CZK": "CZK",
+ "DAI": "DAI",
+ "DASH": "DASH",
+ "DJF": "DJF",
+ "DKK": "DKK",
+ "DNT": "DNT",
+ "DOP": "DOP",
+ "DZD": "DZD",
+ "EGP": "EGP",
+ "ENJ": "ENJ",
+ "EOS": "EOS",
+ "ERN": "ERN",
+ "ETB": "ETB",
+ "ETC": "ETC",
+ "ETH": "ETH",
+ "ETH2": "ETH2",
+ "EUR": "EUR",
+ "FIL": "FIL",
+ "FJD": "FJD",
+ "FKP": "FKP",
+ "FORTH": "FORTH",
+ "GBP": "GBP",
+ "GBX": "GBX",
+ "GEL": "GEL",
+ "GGP": "GGP",
+ "GHS": "GHS",
+ "GIP": "GIP",
+ "GMD": "GMD",
+ "GNF": "GNF",
+ "GRT": "GRT",
+ "GTQ": "GTQ",
+ "GYD": "GYD",
+ "HKD": "HKD",
+ "HNL": "HNL",
+ "HRK": "HRK",
+ "HTG": "HTG",
+ "HUF": "HUF",
+ "IDR": "IDR",
+ "ILS": "ILS",
+ "IMP": "IMP",
+ "INR": "INR",
+ "IQD": "IQD",
+ "ISK": "ISK",
+ "JEP": "JEP",
+ "JMD": "JMD",
+ "JOD": "JOD",
+ "JPY": "JPY",
+ "KES": "KES",
+ "KGS": "KGS",
+ "KHR": "KHR",
+ "KMF": "KMF",
+ "KNC": "KNC",
+ "KRW": "KRW",
+ "KWD": "KWD",
+ "KYD": "KYD",
+ "KZT": "KZT",
+ "LAK": "LAK",
+ "LBP": "LBP",
+ "LINK": "LINK",
+ "LKR": "LKR",
+ "LRC": "LRC",
+ "LRD": "LRD",
+ "LSL": "LSL",
+ "LTC": "LTC",
+ "LYD": "LYD",
+ "MAD": "MAD",
+ "MANA": "MANA",
+ "MATIC": "MATIC",
+ "MDL": "MDL",
+ "MGA": "MGA",
+ "MKD": "MKD",
+ "MKR": "MKR",
+ "MMK": "MMK",
+ "MNT": "MNT",
+ "MOP": "MOP",
+ "MRO": "MRO",
+ "MTL": "MTL",
+ "MUR": "MUR",
+ "MVR": "MVR",
+ "MWK": "MWK",
+ "MXN": "MXN",
+ "MYR": "MYR",
+ "MZN": "MZN",
+ "NAD": "NAD",
+ "NGN": "NGN",
+ "NIO": "NIO",
+ "NKN": "NKN",
+ "NMR": "NMR",
+ "NOK": "NOK",
+ "NPR": "NPR",
+ "NU": "NU",
+ "NZD": "NZD",
+ "OGN": "OGN",
+ "OMG": "OMG",
+ "OMR": "OMR",
+ "OXT": "OXT",
+ "PAB": "PAB",
+ "PEN": "PEN",
+ "PGK": "PGK",
+ "PHP": "PHP",
+ "PKR": "PKR",
+ "PLN": "PLN",
+ "PYG": "PYG",
+ "QAR": "QAR",
+ "REN": "REN",
+ "REP": "REP",
+ "RON": "RON",
+ "RSD": "RSD",
+ "RUB": "RUB",
+ "RWF": "RWF",
+ "SAR": "SAR",
+ "SBD": "SBD",
+ "SCR": "SCR",
+ "SEK": "SEK",
+ "SGD": "SGD",
+ "SHP": "SHP",
+ "SKL": "SKL",
+ "SLL": "SLL",
+ "SNX": "SNX",
+ "SOS": "SOS",
+ "SRD": "SRD",
+ "SSP": "SSP",
+ "STD": "STD",
+ "STORJ": "STORJ",
+ "SUSHI": "SUSHI",
+ "SVC": "SVC",
+ "SZL": "SZL",
+ "THB": "THB",
+ "TJS": "TJS",
+ "TMT": "TMT",
+ "TND": "TND",
+ "TOP": "TOP",
+ "TRY": "TRY",
+ "TTD": "TTD",
+ "TWD": "TWD",
+ "TZS": "TZS",
+ "UAH": "UAH",
+ "UGX": "UGX",
+ "UMA": "UMA",
+ "UNI": "UNI",
+ "USD": "USD",
+ "USDC": "USDC",
+ "UYU": "UYU",
+ "UZS": "UZS",
+ "VES": "VES",
+ "VND": "VND",
+ "VUV": "VUV",
+ "WBTC": "WBTC",
+ "WST": "WST",
+ "XAF": "XAF",
+ "XAG": "XAG",
+ "XAU": "XAU",
+ "XCD": "XCD",
+ "XDR": "XDR",
+ "XLM": "XLM",
+ "XOF": "XOF",
+ "XPD": "XPD",
+ "XPF": "XPF",
+ "XPT": "XPT",
+ "XTZ": "XTZ",
+ "YER": "YER",
+ "YFI": "YFI",
+ "ZAR": "ZAR",
+ "ZEC": "ZEC",
+ "ZMW": "ZMW",
+ "ZRX": "ZRX",
+ "ZWL": "ZWL",
+}
diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json
index 4579aecdd5b..aa056409786 100644
--- a/homeassistant/components/coinbase/manifest.json
+++ b/homeassistant/components/coinbase/manifest.json
@@ -2,7 +2,12 @@
"domain": "coinbase",
"name": "Coinbase",
"documentation": "https://www.home-assistant.io/integrations/coinbase",
- "requirements": ["coinbase==2.1.0"],
- "codeowners": [],
+ "requirements": [
+ "coinbase==2.1.0"
+ ],
+ "codeowners": [
+ "@tombrien"
+ ],
+ "config_flow": true,
"iot_class": "cloud_polling"
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py
index 3a0e689862f..13981619051 100644
--- a/homeassistant/components/coinbase/sensor.py
+++ b/homeassistant/components/coinbase/sensor.py
@@ -1,7 +1,24 @@
"""Support for Coinbase sensors."""
+import logging
+
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION
+from .const import (
+ API_ACCOUNT_AMOUNT,
+ API_ACCOUNT_BALANCE,
+ API_ACCOUNT_CURRENCY,
+ API_ACCOUNT_ID,
+ API_ACCOUNT_NAME,
+ API_ACCOUNT_NATIVE_BALANCE,
+ API_RATES,
+ CONF_CURRENCIES,
+ CONF_EXCHANGE_RATES,
+ DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
ATTR_NATIVE_BALANCE = "Balance in native currency"
CURRENCY_ICONS = {
@@ -16,45 +33,81 @@ DEFAULT_COIN_ICON = "mdi:currency-usd-circle"
ATTRIBUTION = "Data provided by coinbase.com"
-DATA_COINBASE = "coinbase_cache"
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Coinbase sensor platform."""
+ instance = hass.data[DOMAIN][config_entry.entry_id]
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Coinbase sensors."""
- if discovery_info is None:
- return
- if "account" in discovery_info:
- account = discovery_info["account"]
- sensor = AccountSensor(
- hass.data[DATA_COINBASE], account["name"], account["balance"]["currency"]
- )
- if "exchange_currency" in discovery_info:
- sensor = ExchangeRateSensor(
- hass.data[DATA_COINBASE],
- discovery_info["exchange_currency"],
- discovery_info["native_currency"],
- )
+ entities = []
- add_entities([sensor], True)
+ provided_currencies = [
+ account[API_ACCOUNT_CURRENCY] for account in instance.accounts
+ ]
+
+ desired_currencies = []
+
+ if CONF_CURRENCIES in config_entry.options:
+ desired_currencies = config_entry.options[CONF_CURRENCIES]
+
+ exchange_native_currency = instance.exchange_rates[API_ACCOUNT_CURRENCY]
+
+ for currency in desired_currencies:
+ if currency not in provided_currencies:
+ _LOGGER.warning(
+ "The currency %s is no longer provided by your account, please check "
+ "your settings in Coinbase's developer tools",
+ currency,
+ )
+ continue
+ entities.append(AccountSensor(instance, currency))
+
+ if CONF_EXCHANGE_RATES in config_entry.options:
+ for rate in config_entry.options[CONF_EXCHANGE_RATES]:
+ entities.append(
+ ExchangeRateSensor(
+ instance,
+ rate,
+ exchange_native_currency,
+ )
+ )
+
+ async_add_entities(entities)
class AccountSensor(SensorEntity):
"""Representation of a Coinbase.com sensor."""
- def __init__(self, coinbase_data, name, currency):
+ def __init__(self, coinbase_data, currency):
"""Initialize the sensor."""
self._coinbase_data = coinbase_data
- self._name = f"Coinbase {name}"
- self._state = None
- self._unit_of_measurement = currency
- self._native_balance = None
- self._native_currency = None
+ self._currency = currency
+ for account in coinbase_data.accounts:
+ if account[API_ACCOUNT_CURRENCY] == currency:
+ self._name = f"Coinbase {account[API_ACCOUNT_NAME]}"
+ self._id = (
+ f"coinbase-{account[API_ACCOUNT_ID]}-wallet-"
+ f"{account[API_ACCOUNT_CURRENCY]}"
+ )
+ self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
+ self._unit_of_measurement = account[API_ACCOUNT_CURRENCY]
+ self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][
+ API_ACCOUNT_AMOUNT
+ ]
+ self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][
+ API_ACCOUNT_CURRENCY
+ ]
+ break
@property
def name(self):
"""Return the name of the sensor."""
return self._name
+ @property
+ def unique_id(self):
+ """Return the Unique ID of the sensor."""
+ return self._id
+
@property
def state(self):
"""Return the state of the sensor."""
@@ -82,10 +135,15 @@ class AccountSensor(SensorEntity):
"""Get the latest state of the sensor."""
self._coinbase_data.update()
for account in self._coinbase_data.accounts:
- if self._name == f"Coinbase {account['name']}":
- self._state = account["balance"]["amount"]
- self._native_balance = account["native_balance"]["amount"]
- self._native_currency = account["native_balance"]["currency"]
+ if account[API_ACCOUNT_CURRENCY] == self._currency:
+ self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
+ self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][
+ API_ACCOUNT_AMOUNT
+ ]
+ self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][
+ API_ACCOUNT_CURRENCY
+ ]
+ break
class ExchangeRateSensor(SensorEntity):
@@ -96,7 +154,10 @@ class ExchangeRateSensor(SensorEntity):
self._coinbase_data = coinbase_data
self.currency = exchange_currency
self._name = f"{exchange_currency} Exchange Rate"
- self._state = None
+ self._id = f"coinbase-{coinbase_data.user_id}-xe-{exchange_currency}"
+ self._state = round(
+ 1 / float(self._coinbase_data.exchange_rates[API_RATES][self.currency]), 2
+ )
self._unit_of_measurement = native_currency
@property
@@ -104,6 +165,11 @@ class ExchangeRateSensor(SensorEntity):
"""Return the name of the sensor."""
return self._name
+ @property
+ def unique_id(self):
+ """Return the unique ID of the sensor."""
+ return self._id
+
@property
def state(self):
"""Return the state of the sensor."""
@@ -127,5 +193,6 @@ class ExchangeRateSensor(SensorEntity):
def update(self):
"""Get the latest state of the sensor."""
self._coinbase_data.update()
- rate = self._coinbase_data.exchange_rates.rates[self.currency]
- self._state = round(1 / float(rate), 2)
+ self._state = round(
+ 1 / float(self._coinbase_data.exchange_rates.rates[self.currency]), 2
+ )
diff --git a/homeassistant/components/coinbase/strings.json b/homeassistant/components/coinbase/strings.json
new file mode 100644
index 00000000000..399bfbd894a
--- /dev/null
+++ b/homeassistant/components/coinbase/strings.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "Coinbase API Key Details",
+ "description": "Please enter the details of your API key as provided by Coinbase.",
+ "data": {
+ "api_key": "[%key:common::config_flow::data::api_key%]",
+ "api_token": "API Secret"
+ }
+ }
+ },
+ "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": {
+ "description": "Adjust Coinbase Options",
+ "data": {
+ "account_balance_currencies": "Wallet balances to report.",
+ "exchange_rate_currencies": "Exchange rates to report."
+ }
+ }
+ },
+ "error": {
+ "unknown": "[%key:common::config_flow::error::unknown%]",
+ "currency_unavaliable": "One or more of the requested currency balances is not provided by your Coinbase API.",
+ "exchange_rate_unavaliable": "One or more of the requested exchange rates is not provided by Coinbase."
+ }
+ }
+}
diff --git a/homeassistant/components/coinbase/translations/de.json b/homeassistant/components/coinbase/translations/de.json
new file mode 100644
index 00000000000..37ccdedd81a
--- /dev/null
+++ b/homeassistant/components/coinbase/translations/de.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-Schl\u00fcssel",
+ "api_token": "API-Geheimnis",
+ "currencies": "Kontostand W\u00e4hrungen",
+ "exchange_rates": "Wechselkurse"
+ },
+ "description": "Bitte gib die Details Ihres API-Schl\u00fcssels ein, wie von Coinbase bereitgestellt. Trenne mehrere W\u00e4hrungen mit einem Komma (z. B. \"BTC, EUR\")",
+ "title": "Coinbase API Schl\u00fcssel Details"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "currency_unavaliable": "Eine oder mehrere der angeforderten W\u00e4hrungssalden werden von Ihrer Coinbase-API nicht bereitgestellt.",
+ "exchange_rate_unavaliable": "Einer oder mehrere der angeforderten Wechselkurse werden nicht von Coinbase bereitgestellt.",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "account_balance_currencies": "Zu meldende Wallet-Guthaben.",
+ "exchange_rate_currencies": "Zu meldende Wechselkurse."
+ },
+ "description": "Coinbase-Optionen anpassen"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json
new file mode 100644
index 00000000000..12db6bf8a30
--- /dev/null
+++ b/homeassistant/components/coinbase/translations/en.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key",
+ "api_token": "API Secret",
+ "currencies": "Account Balance Currencies",
+ "exchange_rates": "Exchange Rates"
+ },
+ "description": "Please enter the details of your API key as provided by Coinbase.",
+ "title": "Coinbase API Key Details"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "currency_unavaliable": "One or more of the requested currency balances is not provided by your Coinbase API.",
+ "exchange_rate_unavaliable": "One or more of the requested exchange rates is not provided by Coinbase.",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "account_balance_currencies": "Wallet balances to report.",
+ "exchange_rate_currencies": "Exchange rates to report."
+ },
+ "description": "Adjust Coinbase Options"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coinbase/translations/et.json b/homeassistant/components/coinbase/translations/et.json
new file mode 100644
index 00000000000..ce5ea46ce34
--- /dev/null
+++ b/homeassistant/components/coinbase/translations/et.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine",
+ "unknown": "Ootamtu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API v\u00f5ti",
+ "api_token": "API salas\u00f5na",
+ "currencies": "Konto saldo valuutad",
+ "exchange_rates": "Vahetuskursid"
+ },
+ "description": "Sisesta Coinbase'i pakutava API-v\u00f5tme \u00fcksikasjad.",
+ "title": "Coinbase'i API v\u00f5tme \u00fcksikasjad"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "currency_unavaliable": "Coinbase'i API ei paku \u00fchte v\u00f5i mitut taotletud valuutasaldot.",
+ "exchange_rate_unavaliable": "\u00dchte v\u00f5i mitut taotletud vahetuskurssi Coinbase ei paku.",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "account_balance_currencies": "Rahakoti saldod teavitamine.",
+ "exchange_rate_currencies": "Vahetuskursside aruanne."
+ },
+ "description": "Kohanda Coinbase'i valikuid"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coinbase/translations/nl.json b/homeassistant/components/coinbase/translations/nl.json
new file mode 100644
index 00000000000..052caf2f358
--- /dev/null
+++ b/homeassistant/components/coinbase/translations/nl.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-sleutel",
+ "api_token": "API-geheim",
+ "currencies": "Valuta's van rekeningsaldo",
+ "exchange_rates": "Wisselkoersen"
+ },
+ "description": "Voer de gegevens van uw API-sleutel in zoals verstrekt door Coinbase. Scheidt meerdere valuta's met een komma (bijv. \"BTC, EUR\")",
+ "title": "Coinbase API Sleutel Details"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "currency_unavaliable": "Een of meer van de gevraagde valutasaldi worden niet geleverd door uw Coinbase API.",
+ "exchange_rate_unavaliable": "Een of meer van de gevraagde wisselkoersen worden niet door Coinbase verstrekt.",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "account_balance_currencies": "Wallet-saldi om te rapporteren.",
+ "exchange_rate_currencies": "Wisselkoersen om te rapporteren."
+ },
+ "description": "Coinbase-opties aanpassen"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coinbase/translations/no.json b/homeassistant/components/coinbase/translations/no.json
new file mode 100644
index 00000000000..265ea29e01c
--- /dev/null
+++ b/homeassistant/components/coinbase/translations/no.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-n\u00f8kkel",
+ "api_token": "API-hemmelighet",
+ "currencies": "Valutaer for kontosaldo",
+ "exchange_rates": "Valutakurser"
+ },
+ "description": "Vennligst skriv inn detaljene for API-n\u00f8kkelen din som gitt av Coinbase. Skill flere valutaer med komma (f.eks. \"BTC, EUR\")",
+ "title": "Detaljer for Coinbase API-n\u00f8kkel"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "currency_unavaliable": "En eller flere av de forespurte valutasaldoene leveres ikke av Coinbase API.",
+ "exchange_rate_unavaliable": "En eller flere av de forespurte valutakursene leveres ikke av Coinbase.",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "account_balance_currencies": "Lommeboksaldoer som skal rapporteres.",
+ "exchange_rate_currencies": "Valutakurser som skal rapporteres."
+ },
+ "description": "Juster Coinbase-alternativer"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coinbase/translations/ru.json b/homeassistant/components/coinbase/translations/ru.json
new file mode 100644
index 00000000000..93bb203d24b
--- /dev/null
+++ b/homeassistant/components/coinbase/translations/ru.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "api_token": "\u0421\u0435\u043a\u0440\u0435\u0442 API",
+ "currencies": "\u041e\u0441\u0442\u0430\u0442\u043e\u043a \u0432\u0430\u043b\u044e\u0442\u044b \u043d\u0430 \u0441\u0447\u0435\u0442\u0435",
+ "exchange_rates": "\u041e\u0431\u043c\u0435\u043d\u043d\u044b\u0435 \u043a\u0443\u0440\u0441\u044b"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0412\u0430\u0448\u0435\u0433\u043e \u043a\u043b\u044e\u0447\u0430 API Coinbase.",
+ "title": "\u041a\u043b\u044e\u0447 API Coinbase"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "currency_unavaliable": "\u041e\u0434\u0438\u043d \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0445 \u043e\u0441\u0442\u0430\u0442\u043a\u043e\u0432 \u0432\u0430\u043b\u044e\u0442\u044b \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0412\u0430\u0448\u0438\u043c API Coinbase.",
+ "exchange_rate_unavaliable": "Coinbase \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0434\u0438\u043d \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0445 \u043e\u0431\u043c\u0435\u043d\u043d\u044b\u0445 \u043a\u0443\u0440\u0441\u043e\u0432.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "account_balance_currencies": "\u0411\u0430\u043b\u0430\u043d\u0441\u044b \u043a\u043e\u0448\u0435\u043b\u044c\u043a\u0430 \u0434\u043b\u044f \u043e\u0442\u0447\u0435\u0442\u043d\u043e\u0441\u0442\u0438.",
+ "exchange_rate_currencies": "\u041a\u0443\u0440\u0441\u044b \u0432\u0430\u043b\u044e\u0442 \u0434\u043b\u044f \u043e\u0442\u0447\u0435\u0442\u043d\u043e\u0441\u0442\u0438."
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 Coinbase"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coinbase/translations/zh-Hant.json b/homeassistant/components/coinbase/translations/zh-Hant.json
new file mode 100644
index 00000000000..aa00e459591
--- /dev/null
+++ b/homeassistant/components/coinbase/translations/zh-Hant.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \u5bc6\u9470",
+ "api_token": "API \u5bc6\u9470",
+ "currencies": "\u5e33\u6236\u9918\u984d\u8ca8\u5e63",
+ "exchange_rates": "\u532f\u7387"
+ },
+ "description": "\u8acb\u8f38\u5165\u7531 Coinbase \u63d0\u4f9b\u7684 API \u5bc6\u9470\u8cc7\u8a0a\u3002\u4ee5\u9017\u865f\u5206\u9694\u591a\u7a2e\u8ca8\u5e63\uff08\u4f8b\u5982 \"BTC, EUR\"\uff09",
+ "title": "Coinbase API \u5bc6\u9470\u8cc7\u6599"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "currency_unavaliable": "Coinbase API \u672a\u63d0\u4f9b\u4e00\u500b\u6216\u591a\u500b\u6240\u8981\u6c42\u7684\u8ca8\u5e63\u9918\u984d\u3002",
+ "exchange_rate_unavaliable": "Coinbase \u672a\u63d0\u4f9b\u4e00\u500b\u6216\u591a\u500b\u6240\u8981\u6c42\u7684\u532f\u7387\u3002",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "account_balance_currencies": "\u5e33\u6236\u9918\u984d\u56de\u5831\u503c\u3002",
+ "exchange_rate_currencies": "\u532f\u7387\u56de\u5831\u503c\u3002"
+ },
+ "description": "\u8abf\u6574 Coinbase \u9078\u9805"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py
index 78e27d86f8e..6e4af61e24b 100644
--- a/homeassistant/components/control4/__init__.py
+++ b/homeassistant/components/control4/__init__.py
@@ -41,7 +41,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["light"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Control4 from a config entry."""
hass.data.setdefault(DOMAIN, {})
entry_data = hass.data[DOMAIN].setdefault(entry.entry_id, {})
diff --git a/homeassistant/components/control4/translations/he.json b/homeassistant/components/control4/translations/he.json
new file mode 100644
index 00000000000..c7f019363ff
--- /dev/null
+++ b/homeassistant/components/control4/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "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",
+ "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": {
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py
index e6cf6f36277..1bcf20f4d5e 100644
--- a/homeassistant/components/coolmaster/__init__.py
+++ b/homeassistant/components/coolmaster/__init__.py
@@ -24,7 +24,7 @@ async def async_setup_entry(hass, entry):
info = await coolmaster.info()
if not info:
raise ConfigEntryNotReady
- except (OSError, ConnectionRefusedError, TimeoutError) as error:
+ except OSError as error:
raise ConfigEntryNotReady() from error
coordinator = CoolmasterDataUpdateCoordinator(hass, coolmaster)
hass.data.setdefault(DOMAIN, {})
@@ -64,5 +64,5 @@ class CoolmasterDataUpdateCoordinator(DataUpdateCoordinator):
"""Fetch data from Coolmaster."""
try:
return await self._coolmaster.status()
- except (OSError, ConnectionRefusedError, TimeoutError) as error:
+ except OSError as error:
raise UpdateFailed from error
diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py
index 1091c24ea31..6a5c517fc85 100644
--- a/homeassistant/components/coolmaster/config_flow.py
+++ b/homeassistant/components/coolmaster/config_flow.py
@@ -51,7 +51,7 @@ class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
result = await _validate_connection(self.hass, host)
if not result:
errors["base"] = "no_units"
- except (OSError, ConnectionRefusedError, TimeoutError):
+ except OSError:
errors["base"] = "cannot_connect"
if errors:
diff --git a/homeassistant/components/coolmaster/translations/he.json b/homeassistant/components/coolmaster/translations/he.json
new file mode 100644
index 00000000000..5903faf3c72
--- /dev/null
+++ b/homeassistant/components/coolmaster/translations/he.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "no_units": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05de\u05e6\u05d5\u05d0 \u05d9\u05d7\u05d9\u05d3\u05d5\u05ea HVAC \u05d1\u05de\u05d0\u05e8\u05d7 CoolMasterNet."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coronavirus/translations/he.json b/homeassistant/components/coronavirus/translations/he.json
new file mode 100644
index 00000000000..5ac1be49cfb
--- /dev/null
+++ b/homeassistant/components/coronavirus/translations/he.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "country": "\u05de\u05d3\u05d9\u05e0\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py
index 034beb7f9db..110dd09098e 100644
--- a/homeassistant/components/cover/__init__.py
+++ b/homeassistant/components/cover/__init__.py
@@ -1,4 +1,6 @@
"""Support for Cover devices."""
+from __future__ import annotations
+
from datetime import timedelta
import functools as ft
import logging
@@ -6,6 +8,7 @@ from typing import Any, final
import voluptuous as vol
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
SERVICE_CLOSE_COVER,
SERVICE_CLOSE_COVER_TILT,
@@ -22,6 +25,7 @@ from homeassistant.const import (
STATE_OPEN,
STATE_OPENING,
)
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
@@ -154,35 +158,47 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class CoverEntity(Entity):
"""Base class for cover entities."""
+ _attr_current_cover_position: int | None = None
+ _attr_current_cover_tilt_position: int | None = None
+ _attr_is_closed: bool | None
+ _attr_is_closing: bool | None = None
+ _attr_is_opening: bool | None = None
+ _attr_state: None = None
+
@property
- def current_cover_position(self):
+ def current_cover_position(self) -> int | None:
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
+ return self._attr_current_cover_position
@property
- def current_cover_tilt_position(self):
+ def current_cover_tilt_position(self) -> int | None:
"""Return current position of cover tilt.
None is unknown, 0 is closed, 100 is fully open.
"""
+ return self._attr_current_cover_tilt_position
@property
- def state(self):
+ @final
+ def state(self) -> str | None:
"""Return the state of the cover."""
if self.is_opening:
return STATE_OPENING
@@ -213,8 +229,11 @@ class CoverEntity(Entity):
return data
@property
- def supported_features(self):
+ def supported_features(self) -> int:
"""Flag supported features."""
+ if self._attr_supported_features is not None:
+ return self._attr_supported_features
+
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
if self.current_cover_position is not None:
@@ -231,17 +250,19 @@ class CoverEntity(Entity):
return supported_features
@property
- def is_opening(self):
+ def is_opening(self) -> bool | None:
"""Return if the cover is opening or not."""
+ return self._attr_is_opening
@property
- def is_closing(self):
+ def is_closing(self) -> bool | None:
"""Return if the cover is closing or not."""
+ return self._attr_is_closing
@property
- def is_closed(self):
+ def is_closed(self) -> bool | None:
"""Return if the cover is closed or not."""
- raise NotImplementedError()
+ return self._attr_is_closed
def open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
diff --git a/homeassistant/components/cover/device_action.py b/homeassistant/components/cover/device_action.py
index 74eef8102df..13ef4523f5b 100644
--- a/homeassistant/components/cover/device_action.py
+++ b/homeassistant/components/cover/device_action.py
@@ -5,7 +5,6 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
- ATTR_SUPPORTED_FEATURES,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
@@ -21,6 +20,7 @@ from homeassistant.const import (
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import get_supported_features
from . import (
ATTR_POSITION,
@@ -68,79 +68,32 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
if entry.domain != DOMAIN:
continue
- state = hass.states.get(entry.entity_id)
- if not state or ATTR_SUPPORTED_FEATURES not in state.attributes:
- continue
-
- supported_features = state.attributes[ATTR_SUPPORTED_FEATURES]
+ supported_features = get_supported_features(hass, entry.entity_id)
# Add actions for each entity that belongs to this integration
+ base_action = {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
if supported_features & SUPPORT_SET_POSITION:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "set_position",
- }
- )
+ actions.append({**base_action, CONF_TYPE: "set_position"})
else:
if supported_features & SUPPORT_OPEN:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "open",
- }
- )
+ actions.append({**base_action, CONF_TYPE: "open"})
if supported_features & SUPPORT_CLOSE:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "close",
- }
- )
+ actions.append({**base_action, CONF_TYPE: "close"})
if supported_features & SUPPORT_STOP:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "stop",
- }
- )
+ actions.append({**base_action, CONF_TYPE: "stop"})
if supported_features & SUPPORT_SET_TILT_POSITION:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "set_tilt_position",
- }
- )
+ actions.append({**base_action, CONF_TYPE: "set_tilt_position"})
else:
if supported_features & SUPPORT_OPEN_TILT:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "open_tilt",
- }
- )
+ actions.append({**base_action, CONF_TYPE: "open_tilt"})
if supported_features & SUPPORT_CLOSE_TILT:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "close_tilt",
- }
- )
+ actions.append({**base_action, CONF_TYPE: "close_tilt"})
return actions
diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py
index 2943f589f7b..bd433dbd93d 100644
--- a/homeassistant/components/cover/device_condition.py
+++ b/homeassistant/components/cover/device_condition.py
@@ -7,7 +7,6 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
- ATTR_SUPPORTED_FEATURES,
CONF_ABOVE,
CONF_BELOW,
CONF_CONDITION,
@@ -28,6 +27,7 @@ from homeassistant.helpers import (
template,
)
from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
+from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import (
@@ -77,71 +77,25 @@ async def async_get_conditions(hass: HomeAssistant, device_id: str) -> list[dict
if entry.domain != DOMAIN:
continue
- state = hass.states.get(entry.entity_id)
- if not state or ATTR_SUPPORTED_FEATURES not in state.attributes:
- continue
-
- supported_features = state.attributes[ATTR_SUPPORTED_FEATURES]
+ supported_features = get_supported_features(hass, entry.entity_id)
supports_open_close = supported_features & (SUPPORT_OPEN | SUPPORT_CLOSE)
# Add conditions for each entity that belongs to this integration
+ base_condition = {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
if supports_open_close:
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_open",
- }
- )
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_closed",
- }
- )
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_opening",
- }
- )
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_closing",
- }
- )
+ conditions += [
+ {**base_condition, CONF_TYPE: cond} for cond in STATE_CONDITION_TYPES
+ ]
if supported_features & SUPPORT_SET_POSITION:
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_position",
- }
- )
+ conditions.append({**base_condition, CONF_TYPE: "is_position"})
if supported_features & SUPPORT_SET_TILT_POSITION:
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_tilt_position",
- }
- )
+ conditions.append({**base_condition, CONF_TYPE: "is_tilt_position"})
return conditions
diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py
index 9b94833bb29..acfd276d1fb 100644
--- a/homeassistant/components/cover/device_trigger.py
+++ b/homeassistant/components/cover/device_trigger.py
@@ -4,13 +4,12 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import (
numeric_state as numeric_state_trigger,
state as state_trigger,
)
from homeassistant.const import (
- ATTR_SUPPORTED_FEATURES,
CONF_ABOVE,
CONF_BELOW,
CONF_DEVICE_ID,
@@ -27,6 +26,7 @@ from homeassistant.const import (
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_registry
+from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.typing import ConfigType
from . import (
@@ -41,7 +41,7 @@ POSITION_TRIGGER_TYPES = {"position", "tilt_position"}
STATE_TRIGGER_TYPES = {"opened", "closed", "opening", "closing"}
POSITION_TRIGGER_SCHEMA = vol.All(
- TRIGGER_BASE_SCHEMA.extend(
+ DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(POSITION_TRIGGER_TYPES),
@@ -56,7 +56,7 @@ POSITION_TRIGGER_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE),
)
-STATE_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+STATE_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(STATE_TRIGGER_TYPES),
@@ -77,11 +77,7 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
if entry.domain != DOMAIN:
continue
- state = hass.states.get(entry.entity_id)
- if not state or ATTR_SUPPORTED_FEATURES not in state.attributes:
- continue
-
- supported_features = state.attributes[ATTR_SUPPORTED_FEATURES]
+ supported_features = get_supported_features(hass, entry.entity_id)
supports_open_close = supported_features & (SUPPORT_OPEN | SUPPORT_CLOSE)
# Add triggers for each entity that belongs to this integration
diff --git a/homeassistant/components/cover/translations/he.json b/homeassistant/components/cover/translations/he.json
index ebc7d39b450..fce73cc1698 100644
--- a/homeassistant/components/cover/translations/he.json
+++ b/homeassistant/components/cover/translations/he.json
@@ -1,4 +1,9 @@
{
+ "device_automation": {
+ "action_type": {
+ "stop": "\u05e2\u05e6\u05d5\u05e8 {entity_name}"
+ }
+ },
"state": {
"_": {
"closed": "\u05e0\u05e1\u05d2\u05e8",
diff --git a/homeassistant/components/cover/translations/nl.json b/homeassistant/components/cover/translations/nl.json
index 8b1ca3c3500..c3998187c4f 100644
--- a/homeassistant/components/cover/translations/nl.json
+++ b/homeassistant/components/cover/translations/nl.json
@@ -6,7 +6,7 @@
"open": "Open {entity_name}",
"open_tilt": "Open de kanteling {entity_name}",
"set_position": "Stel de positie van {entity_name} in",
- "set_tilt_position": "Stel de {entity_name} kantelpositie in",
+ "set_tilt_position": "Stel de kantelpositie van {entity_name} in",
"stop": "Stop {entity_name}"
},
"condition_type": {
@@ -35,5 +35,5 @@
"stopped": "Gestopt"
}
},
- "title": "Bedekking"
+ "title": "Rolluik"
}
\ No newline at end of file
diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py
index 63b0c7de25e..504f8cd9f86 100644
--- a/homeassistant/components/daikin/__init__.py
+++ b/homeassistant/components/daikin/__init__.py
@@ -27,7 +27,7 @@ PLATFORMS = ["climate", "sensor", "switch"]
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Establish connection with Daikin."""
conf = entry.data
# For backwards compat, set unique ID
diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py
index 7a60cb4c3b2..d17c3fc0d93 100644
--- a/homeassistant/components/daikin/climate.py
+++ b/homeassistant/components/daikin/climate.py
@@ -9,6 +9,10 @@ from homeassistant.components.climate.const import (
ATTR_HVAC_MODE,
ATTR_PRESET_MODE,
ATTR_SWING_MODE,
+ CURRENT_HVAC_COOL,
+ CURRENT_HVAC_HEAT,
+ CURRENT_HVAC_IDLE,
+ CURRENT_HVAC_OFF,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
HVAC_MODE_FAN_ONLY,
@@ -60,6 +64,12 @@ DAIKIN_TO_HA_STATE = {
"off": HVAC_MODE_OFF,
}
+HA_STATE_TO_CURRENT_HVAC = {
+ HVAC_MODE_COOL: CURRENT_HVAC_COOL,
+ HVAC_MODE_HEAT: CURRENT_HVAC_HEAT,
+ HVAC_MODE_OFF: CURRENT_HVAC_OFF,
+}
+
HA_PRESET_TO_DAIKIN = {
PRESET_AWAY: "on",
PRESET_NONE: "off",
@@ -188,6 +198,18 @@ class DaikinClimate(ClimateEntity):
"""Set new target temperature."""
await self._set(kwargs)
+ @property
+ def hvac_action(self):
+ """Return the current state."""
+ ret = HA_STATE_TO_CURRENT_HVAC.get(self.hvac_mode)
+ if (
+ ret in (CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT)
+ and self._api.device.support_compressor_frequency
+ and self._api.device.compressor_frequency == 0
+ ):
+ return CURRENT_HVAC_IDLE
+ return ret
+
@property
def hvac_mode(self):
"""Return current operation ie. heat, cool, idle."""
diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py
index 5b4bdd28331..b03c8eb113d 100644
--- a/homeassistant/components/daikin/const.py
+++ b/homeassistant/components/daikin/const.py
@@ -5,10 +5,12 @@ from homeassistant.const import (
CONF_NAME,
CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT,
+ DEVICE_CLASS_ENERGY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
ENERGY_KILO_WATT_HOUR,
+ FREQUENCY_HERTZ,
PERCENTAGE,
POWER_KILO_WATT,
TEMP_CELSIUS,
@@ -24,6 +26,7 @@ ATTR_COOL_ENERGY = "cool_energy"
ATTR_HEAT_ENERGY = "heat_energy"
ATTR_HUMIDITY = "humidity"
ATTR_TARGET_HUMIDITY = "target_humidity"
+ATTR_COMPRESSOR_FREQUENCY = "compressor_frequency"
ATTR_STATE_ON = "on"
ATTR_STATE_OFF = "off"
@@ -32,6 +35,7 @@ SENSOR_TYPE_TEMPERATURE = "temperature"
SENSOR_TYPE_HUMIDITY = "humidity"
SENSOR_TYPE_POWER = "power"
SENSOR_TYPE_ENERGY = "energy"
+SENSOR_TYPE_FREQUENCY = "frequency"
SENSOR_TYPES = {
ATTR_INSIDE_TEMPERATURE: {
@@ -68,14 +72,22 @@ SENSOR_TYPES = {
CONF_NAME: "Cool Energy Consumption",
CONF_TYPE: SENSOR_TYPE_ENERGY,
CONF_ICON: "mdi:snowflake",
+ CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
},
ATTR_HEAT_ENERGY: {
CONF_NAME: "Heat Energy Consumption",
CONF_TYPE: SENSOR_TYPE_ENERGY,
CONF_ICON: "mdi:fire",
+ CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
},
+ ATTR_COMPRESSOR_FREQUENCY: {
+ CONF_NAME: "Compressor Frequency",
+ CONF_TYPE: SENSOR_TYPE_FREQUENCY,
+ CONF_ICON: "mdi:fan",
+ CONF_UNIT_OF_MEASUREMENT: FREQUENCY_HERTZ,
+ },
}
CONF_UUID = "uuid"
diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json
index 704cfcf739c..f34dc8edc57 100644
--- a/homeassistant/components/daikin/manifest.json
+++ b/homeassistant/components/daikin/manifest.json
@@ -3,7 +3,7 @@
"name": "Daikin AC",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/daikin",
- "requirements": ["pydaikin==2.4.3"],
+ "requirements": ["pydaikin==2.4.4"],
"codeowners": ["@fredrike"],
"zeroconf": ["_dkapi._tcp.local."],
"quality_scale": "platinum",
diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py
index a5b515ea918..3bfc0a3926c 100644
--- a/homeassistant/components/daikin/sensor.py
+++ b/homeassistant/components/daikin/sensor.py
@@ -10,6 +10,7 @@ from homeassistant.const import (
from . import DOMAIN as DAIKIN_DOMAIN, DaikinApi
from .const import (
+ ATTR_COMPRESSOR_FREQUENCY,
ATTR_COOL_ENERGY,
ATTR_HEAT_ENERGY,
ATTR_HUMIDITY,
@@ -18,6 +19,7 @@ from .const import (
ATTR_TARGET_HUMIDITY,
ATTR_TOTAL_POWER,
SENSOR_TYPE_ENERGY,
+ SENSOR_TYPE_FREQUENCY,
SENSOR_TYPE_HUMIDITY,
SENSOR_TYPE_POWER,
SENSOR_TYPE_TEMPERATURE,
@@ -46,6 +48,8 @@ async def async_setup_entry(hass, entry, async_add_entities):
if daikin_api.device.support_humidity:
sensors.append(ATTR_HUMIDITY)
sensors.append(ATTR_TARGET_HUMIDITY)
+ if daikin_api.device.support_compressor_frequency:
+ sensors.append(ATTR_COMPRESSOR_FREQUENCY)
async_add_entities([DaikinSensor.factory(daikin_api, sensor) for sensor in sensors])
@@ -60,6 +64,7 @@ class DaikinSensor(SensorEntity):
SENSOR_TYPE_HUMIDITY: DaikinClimateSensor,
SENSOR_TYPE_POWER: DaikinPowerSensor,
SENSOR_TYPE_ENERGY: DaikinPowerSensor,
+ SENSOR_TYPE_FREQUENCY: DaikinClimateSensor,
}[SENSOR_TYPES[monitored_state][CONF_TYPE]]
return cls(api, monitored_state)
@@ -125,6 +130,10 @@ class DaikinClimateSensor(DaikinSensor):
return self._api.device.humidity
if self._device_attribute == ATTR_TARGET_HUMIDITY:
return self._api.device.target_humidity
+
+ if self._device_attribute == ATTR_COMPRESSOR_FREQUENCY:
+ return self._api.device.compressor_frequency
+
return None
@@ -135,9 +144,9 @@ class DaikinPowerSensor(DaikinSensor):
def state(self):
"""Return the state of the sensor."""
if self._device_attribute == ATTR_TOTAL_POWER:
- return round(self._api.device.current_total_power_consumption, 3)
+ return round(self._api.device.current_total_power_consumption, 2)
if self._device_attribute == ATTR_COOL_ENERGY:
- return round(self._api.device.last_hour_cool_energy_consumption, 3)
+ return round(self._api.device.last_hour_cool_energy_consumption, 2)
if self._device_attribute == ATTR_HEAT_ENERGY:
- return round(self._api.device.last_hour_heat_energy_consumption, 3)
+ return round(self._api.device.last_hour_heat_energy_consumption, 2)
return None
diff --git a/homeassistant/components/daikin/translations/he.json b/homeassistant/components/daikin/translations/he.json
index 3007c0e968c..0bc64f684fd 100644
--- a/homeassistant/components/daikin/translations/he.json
+++ b/homeassistant/components/daikin/translations/he.json
@@ -1,8 +1,19 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
"user": {
"data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "host": "\u05de\u05d0\u05e8\u05d7",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4"
}
}
diff --git a/homeassistant/components/debugpy/__init__.py b/homeassistant/components/debugpy/__init__.py
index 98f08827c23..613ecfd8ffa 100644
--- a/homeassistant/components/debugpy/__init__.py
+++ b/homeassistant/components/debugpy/__init__.py
@@ -1,7 +1,7 @@
"""The Remote Python Debugger integration."""
from __future__ import annotations
-from asyncio import Event
+from asyncio import Event, get_running_loop
import logging
from threading import Thread
@@ -15,8 +15,8 @@ from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
DOMAIN = "debugpy"
-CONF_WAIT = "wait"
CONF_START = "start"
+CONF_WAIT = "wait"
SERVICE_START = "start"
CONFIG_SCHEMA = vol.Schema(
@@ -43,7 +43,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def debug_start(
call: ServiceCall | None = None, *, wait: bool = True
) -> None:
- """Start the debugger."""
+ """Enable asyncio debugging and start the debugger."""
+ get_running_loop().set_debug(True)
+
debugpy.listen((conf[CONF_HOST], conf[CONF_PORT]))
wait = conf[CONF_WAIT]
diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py
index 8b47363c7ba..1b9a418fb29 100644
--- a/homeassistant/components/deconz/__init__.py
+++ b/homeassistant/components/deconz/__init__.py
@@ -20,8 +20,7 @@ async def async_setup_entry(hass, config_entry):
Load config, group, light and sensor data for server information.
Start websocket for push notification of state changes from deCONZ.
"""
- if DOMAIN not in hass.data:
- hass.data[DOMAIN] = {}
+ hass.data.setdefault(DOMAIN, {})
await async_update_group_unique_id(hass, config_entry)
@@ -33,7 +32,7 @@ async def async_setup_entry(hass, config_entry):
if not await gateway.async_setup():
return False
- hass.data[DOMAIN][config_entry.unique_id] = gateway
+ hass.data[DOMAIN][config_entry.entry_id] = gateway
await gateway.async_update_device_registry()
@@ -48,7 +47,7 @@ async def async_setup_entry(hass, config_entry):
async def async_unload_entry(hass, config_entry):
"""Unload deCONZ config entry."""
- gateway = hass.data[DOMAIN].pop(config_entry.unique_id)
+ gateway = hass.data[DOMAIN].pop(config_entry.entry_id)
if not hass.data[DOMAIN]:
await async_unload_services(hass)
diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py
index 6bb4b72e89d..4fc3e2ad0b8 100644
--- a/homeassistant/components/deconz/alarm_control_panel.py
+++ b/homeassistant/components/deconz/alarm_control_panel.py
@@ -102,35 +102,20 @@ class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity):
TYPE = DOMAIN
+ _attr_code_arm_required = False
+ _attr_supported_features = (
+ SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT
+ )
+
def __init__(self, device, gateway) -> None:
"""Set up alarm control panel device."""
super().__init__(device, gateway)
-
- self._features = SUPPORT_ALARM_ARM_AWAY
- self._features |= SUPPORT_ALARM_ARM_HOME
- self._features |= SUPPORT_ALARM_ARM_NIGHT
-
self._service_to_device_panel_command = {
PANEL_ENTRY_DELAY: self._device.entry_delay,
PANEL_EXIT_DELAY: self._device.exit_delay,
PANEL_NOT_READY_TO_ARM: self._device.not_ready_to_arm,
}
- @property
- def supported_features(self) -> int:
- """Return the list of supported features."""
- return self._features
-
- @property
- def code_arm_required(self) -> bool:
- """Code is not required for arm actions."""
- return False
-
- @property
- def code_format(self) -> None:
- """Code is not supported."""
- return None
-
@callback
def async_update_callback(self, force_update: bool = False) -> None:
"""Update the control panels state."""
@@ -142,7 +127,7 @@ class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity):
super().async_update_callback(force_update=force_update)
@property
- def state(self) -> str:
+ def state(self) -> str | None:
"""Return the state of the control panel."""
return DECONZ_TO_ALARM_STATE.get(self._device.state)
diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py
index de23d06e7db..392d2e03885 100644
--- a/homeassistant/components/deconz/binary_sensor.py
+++ b/homeassistant/components/deconz/binary_sensor.py
@@ -81,6 +81,11 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity):
TYPE = DOMAIN
+ def __init__(self, device, gateway):
+ """Initialize deCONZ binary sensor."""
+ super().__init__(device, gateway)
+ self._attr_device_class = DEVICE_CLASS.get(type(self._device))
+
@callback
def async_update_callback(self, force_update=False):
"""Update the sensor's state."""
@@ -93,11 +98,6 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity):
"""Return true if sensor is on."""
return self._device.state
- @property
- def device_class(self):
- """Return the class of the sensor."""
- return DEVICE_CLASS.get(type(self._device))
-
@property
def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
@@ -127,6 +127,14 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity):
TYPE = DOMAIN
+ _attr_device_class = DEVICE_CLASS_PROBLEM
+
+ def __init__(self, device, gateway):
+ """Initialize deCONZ binary sensor."""
+ super().__init__(device, gateway)
+
+ self._attr_name = f"{self._device.name} Tampered"
+
@property
def unique_id(self) -> str:
"""Return a unique identifier for this device."""
@@ -143,13 +151,3 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity):
def is_on(self) -> bool:
"""Return the state of the sensor."""
return self._device.tampered
-
- @property
- def name(self) -> str:
- """Return the name of the sensor."""
- return f"{self._device.name} Tampered"
-
- @property
- def device_class(self) -> str:
- """Return the class of the sensor."""
- return DEVICE_CLASS_PROBLEM
diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py
index 1ef881e9c90..4f8345b5e92 100644
--- a/homeassistant/components/deconz/climate.py
+++ b/homeassistant/components/deconz/climate.py
@@ -110,6 +110,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity):
"""Representation of a deCONZ thermostat."""
TYPE = DOMAIN
+ _attr_temperature_unit = TEMP_CELSIUS
def __init__(self, device, gateway):
"""Set up thermostat device."""
@@ -127,18 +128,13 @@ class DeconzThermostat(DeconzDevice, ClimateEntity):
value: key for key, value in self._hvac_mode_to_deconz.items()
}
- self._features = SUPPORT_TARGET_TEMPERATURE
+ self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE
if "fanmode" in device.raw["config"]:
- self._features |= SUPPORT_FAN_MODE
+ self._attr_supported_features |= SUPPORT_FAN_MODE
if "preset" in device.raw["config"]:
- self._features |= SUPPORT_PRESET_MODE
-
- @property
- def supported_features(self):
- """Return the list of supported features."""
- return self._features
+ self._attr_supported_features |= SUPPORT_PRESET_MODE
# Fan control
@@ -238,11 +234,6 @@ class DeconzThermostat(DeconzDevice, ClimateEntity):
await self._device.async_set_config(data)
- @property
- def temperature_unit(self):
- """Return the unit of measurement."""
- return TEMP_CELSIUS
-
@property
def extra_state_attributes(self):
"""Return the state attributes of the thermostat."""
diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py
index 799fc221e2c..d2d7025771e 100644
--- a/homeassistant/components/deconz/const.py
+++ b/homeassistant/components/deconz/const.py
@@ -56,8 +56,11 @@ ATTR_ON = "on"
ATTR_VALVE = "valve"
# Covers
-DAMPERS = ["Level controllable output"]
-WINDOW_COVERS = ["Window covering device", "Window covering controller"]
+LEVEL_CONTROLLABLE_OUTPUT = "Level controllable output"
+DAMPERS = [LEVEL_CONTROLLABLE_OUTPUT]
+WINDOW_COVERING_CONTROLLER = "Window covering controller"
+WINDOW_COVERING_DEVICE = "Window covering device"
+WINDOW_COVERS = [WINDOW_COVERING_CONTROLLER, WINDOW_COVERING_DEVICE]
COVER_TYPES = DAMPERS + WINDOW_COVERS
# Fans
diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py
index 68fb9527e87..21618127905 100644
--- a/homeassistant/components/deconz/cover.py
+++ b/homeassistant/components/deconz/cover.py
@@ -18,10 +18,22 @@ from homeassistant.components.cover import (
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from .const import COVER_TYPES, DAMPERS, NEW_LIGHT, WINDOW_COVERS
+from .const import (
+ COVER_TYPES,
+ LEVEL_CONTROLLABLE_OUTPUT,
+ NEW_LIGHT,
+ WINDOW_COVERING_CONTROLLER,
+ WINDOW_COVERING_DEVICE,
+)
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
+DEVICE_CLASS = {
+ LEVEL_CONTROLLABLE_OUTPUT: DEVICE_CLASS_DAMPER,
+ WINDOW_COVERING_CONTROLLER: DEVICE_CLASS_SHADE,
+ WINDOW_COVERING_DEVICE: DEVICE_CLASS_SHADE,
+}
+
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up covers for deCONZ component."""
@@ -61,29 +73,18 @@ class DeconzCover(DeconzDevice, CoverEntity):
"""Set up cover device."""
super().__init__(device, gateway)
- self._features = SUPPORT_OPEN
- self._features |= SUPPORT_CLOSE
- self._features |= SUPPORT_STOP
- self._features |= SUPPORT_SET_POSITION
+ self._attr_supported_features = SUPPORT_OPEN
+ self._attr_supported_features |= SUPPORT_CLOSE
+ self._attr_supported_features |= SUPPORT_STOP
+ self._attr_supported_features |= SUPPORT_SET_POSITION
if self._device.tilt is not None:
- self._features |= SUPPORT_OPEN_TILT
- self._features |= SUPPORT_CLOSE_TILT
- self._features |= SUPPORT_STOP_TILT
- self._features |= SUPPORT_SET_TILT_POSITION
+ self._attr_supported_features |= SUPPORT_OPEN_TILT
+ self._attr_supported_features |= SUPPORT_CLOSE_TILT
+ self._attr_supported_features |= SUPPORT_STOP_TILT
+ self._attr_supported_features |= SUPPORT_SET_TILT_POSITION
- @property
- def supported_features(self):
- """Flag supported features."""
- return self._features
-
- @property
- def device_class(self):
- """Return the class of the cover."""
- if self._device.type in DAMPERS:
- return DEVICE_CLASS_DAMPER
- if self._device.type in WINDOW_COVERS:
- return DEVICE_CLASS_SHADE
+ self._attr_device_class = DEVICE_CLASS.get(self._device.type)
@property
def current_cover_position(self):
diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py
index ab4d4083095..63f624ba643 100644
--- a/homeassistant/components/deconz/deconz_device.py
+++ b/homeassistant/components/deconz/deconz_device.py
@@ -34,8 +34,6 @@ class DeconzBase:
if self.serial is None:
return None
- bridgeid = self.gateway.api.config.bridgeid
-
return {
"connections": {(CONNECTION_ZIGBEE, self.serial)},
"identifiers": {(DECONZ_DOMAIN, self.serial)},
@@ -43,13 +41,15 @@ class DeconzBase:
"model": self._device.modelid,
"name": self._device.name,
"sw_version": self._device.swversion,
- "via_device": (DECONZ_DOMAIN, bridgeid),
+ "via_device": (DECONZ_DOMAIN, self.gateway.api.config.bridgeid),
}
class DeconzDevice(DeconzBase, Entity):
"""Representation of a deCONZ device."""
+ _attr_should_poll = False
+
TYPE = ""
def __init__(self, device, gateway):
@@ -57,16 +57,15 @@ class DeconzDevice(DeconzBase, Entity):
super().__init__(device, gateway)
self.gateway.entities[self.TYPE].add(self.unique_id)
+ self._attr_name = self._device.name
+
@property
- def entity_registry_enabled_default(self):
+ def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry.
Daylight is a virtual sensor from deCONZ that should never be enabled by default.
"""
- if self._device.type == "Daylight":
- return False
-
- return True
+ return self._device.type != "Daylight"
async def async_added_to_hass(self):
"""Subscribe to device events."""
@@ -96,13 +95,3 @@ class DeconzDevice(DeconzBase, Entity):
def available(self):
"""Return True if device is available."""
return self.gateway.available and self._device.reachable
-
- @property
- def name(self):
- """Return the name of the device."""
- return self._device.name
-
- @property
- def should_poll(self):
- """No polling needed."""
- return False
diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py
index 2703adbc139..d7e42808851 100644
--- a/homeassistant/components/deconz/device_trigger.py
+++ b/homeassistant/components/deconz/device_trigger.py
@@ -1,7 +1,7 @@
"""Provides device automations for deconz events."""
import voluptuous as vol
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.device_automation.exceptions import (
InvalidDeviceAutomationConfig,
)
@@ -205,6 +205,13 @@ TRADFRI_REMOTE = {
(CONF_LONG_RELEASE, CONF_RIGHT): {CONF_EVENT: 5003},
}
+TRADFRI_SHORTCUT_REMOTE_MODEL = "TRADFRI SHORTCUT Button"
+TRADFRI_SHORTCUT_REMOTE = {
+ (CONF_SHORT_PRESS, ""): {CONF_EVENT: 1002},
+ (CONF_LONG_PRESS, ""): {CONF_EVENT: 1001},
+ (CONF_LONG_RELEASE, ""): {CONF_EVENT: 1003},
+}
+
TRADFRI_WIRELESS_DIMMER_MODEL = "TRADFRI wireless dimmer"
TRADFRI_WIRELESS_DIMMER = {
(CONF_ROTATED_FAST, CONF_LEFT): {CONF_EVENT: 4002},
@@ -443,6 +450,11 @@ GIRA_JUNG_SWITCH = {
(CONF_SHORT_RELEASE, CONF_BUTTON_8): {CONF_EVENT: 8002},
}
+LIDL_SILVERCREST_DOORBELL_MODEL = "HG06668"
+LIDL_SILVERCREST_DOORBELL = {
+ (CONF_SHORT_PRESS, ""): {CONF_EVENT: 1002},
+}
+
LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL = "Switch-LIGHTIFY"
LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL = "Switch 4x-LIGHTIFY"
LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_EU_MODEL = "Switch 4x EU-LIGHTIFY"
@@ -534,6 +546,7 @@ REMOTES = {
TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH,
TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE,
TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE,
+ TRADFRI_SHORTCUT_REMOTE_MODEL: TRADFRI_SHORTCUT_REMOTE,
TRADFRI_WIRELESS_DIMMER_MODEL: TRADFRI_WIRELESS_DIMMER,
AQARA_CUBE_MODEL: AQARA_CUBE,
AQARA_CUBE_MODEL_ALT1: AQARA_CUBE,
@@ -556,6 +569,7 @@ REMOTES = {
GIRA_JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL,
GIRA_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL,
JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL,
+ LIDL_SILVERCREST_DOORBELL_MODEL: LIDL_SILVERCREST_DOORBELL,
LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE,
LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE,
LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_EU_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE,
@@ -567,7 +581,7 @@ REMOTES = {
UBISYS_CONTROL_UNIT_C4_MODEL: UBISYS_CONTROL_UNIT_C4,
}
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str}
)
diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py
index dfb6802fd75..cb64bff6d16 100644
--- a/homeassistant/components/deconz/fan.py
+++ b/homeassistant/components/deconz/fan.py
@@ -67,7 +67,7 @@ class DeconzFan(DeconzDevice, FanEntity):
if self._device.speed in ORDERED_NAMED_FAN_SPEEDS:
self._default_on_speed = self._device.speed
- self._features = SUPPORT_SET_SPEED
+ self._attr_supported_features = SUPPORT_SET_SPEED
@property
def is_on(self) -> bool:
@@ -128,7 +128,7 @@ class DeconzFan(DeconzDevice, FanEntity):
@property
def supported_features(self) -> int:
"""Flag supported features."""
- return self._features
+ return self._attr_supported_features
@callback
def async_update_callback(self, force_update=False) -> None:
diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py
index 8b057ab9e51..0a7d7e0c849 100644
--- a/homeassistant/components/deconz/gateway.py
+++ b/homeassistant/components/deconz/gateway.py
@@ -33,8 +33,8 @@ from .errors import AuthenticationRequired, CannotConnect
@callback
def get_gateway_from_config_entry(hass, config_entry):
- """Return gateway with a matching bridge id."""
- return hass.data[DECONZ_DOMAIN][config_entry.unique_id]
+ """Return gateway with a matching config entry ID."""
+ return hass.data[DECONZ_DOMAIN][config_entry.entry_id]
class DeconzGateway:
diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py
index 838e7639fc7..90d5e82af71 100644
--- a/homeassistant/components/deconz/light.py
+++ b/homeassistant/components/deconz/light.py
@@ -1,6 +1,6 @@
"""Support for deCONZ lights."""
-from pydeconz.light import Light
+from __future__ import annotations
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -9,13 +9,16 @@ from homeassistant.components.light import (
ATTR_FLASH,
ATTR_HS_COLOR,
ATTR_TRANSITION,
+ ATTR_XY_COLOR,
+ COLOR_MODE_BRIGHTNESS,
+ COLOR_MODE_COLOR_TEMP,
+ COLOR_MODE_HS,
+ COLOR_MODE_ONOFF,
+ COLOR_MODE_XY,
DOMAIN,
EFFECT_COLORLOOP,
FLASH_LONG,
FLASH_SHORT,
- SUPPORT_BRIGHTNESS,
- SUPPORT_COLOR,
- SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
@@ -23,7 +26,6 @@ from homeassistant.components.light import (
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-import homeassistant.util.color as color_util
from .const import (
COVER_TYPES,
@@ -37,6 +39,7 @@ from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
CONTROLLER = ["Configuration tool"]
+DECONZ_GROUP = "is_deconz_group"
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -106,24 +109,44 @@ class DeconzBaseLight(DeconzDevice, LightEntity):
"""Set up light."""
super().__init__(device, gateway)
- self._features = 0
- self.update_features(self._device)
-
- def update_features(self, device):
- """Calculate supported features of device."""
- if device.brightness is not None:
- self._features |= SUPPORT_BRIGHTNESS
- self._features |= SUPPORT_FLASH
- self._features |= SUPPORT_TRANSITION
+ self._attr_supported_color_modes = set()
if device.ct is not None:
- self._features |= SUPPORT_COLOR_TEMP
+ self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
- if device.xy is not None or (device.hue is not None and device.sat is not None):
- self._features |= SUPPORT_COLOR
+ if device.hue is not None and device.sat is not None:
+ self._attr_supported_color_modes.add(COLOR_MODE_HS)
+
+ if device.xy is not None:
+ self._attr_supported_color_modes.add(COLOR_MODE_XY)
+
+ if not self._attr_supported_color_modes and device.brightness is not None:
+ self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
+
+ if not self._attr_supported_color_modes:
+ self._attr_supported_color_modes.add(COLOR_MODE_ONOFF)
+
+ if device.brightness is not None:
+ self._attr_supported_features |= SUPPORT_FLASH
+ self._attr_supported_features |= SUPPORT_TRANSITION
if device.effect is not None:
- self._features |= SUPPORT_EFFECT
+ self._attr_supported_features |= SUPPORT_EFFECT
+
+ @property
+ def color_mode(self) -> str:
+ """Return the color mode of the light."""
+ if self._device.colormode == "ct":
+ color_mode = COLOR_MODE_COLOR_TEMP
+ elif self._device.colormode == "hs":
+ color_mode = COLOR_MODE_HS
+ elif self._device.colormode == "xy":
+ color_mode = COLOR_MODE_XY
+ elif self._device.brightness is not None:
+ color_mode = COLOR_MODE_BRIGHTNESS
+ else:
+ color_mode = COLOR_MODE_ONOFF
+ return color_mode
@property
def brightness(self):
@@ -138,47 +161,39 @@ class DeconzBaseLight(DeconzDevice, LightEntity):
@property
def color_temp(self):
"""Return the CT color value."""
- if self._device.colormode != "ct":
- return None
-
return self._device.ct
@property
- def hs_color(self):
+ def hs_color(self) -> tuple:
"""Return the hs color value."""
- if self._device.colormode in ("xy", "hs"):
- if self._device.xy:
- return color_util.color_xy_to_hs(*self._device.xy)
- if self._device.hue and self._device.sat:
- return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100)
- return None
+ return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100)
+
+ @property
+ def xy_color(self) -> tuple | None:
+ """Return the XY color value."""
+ return self._device.xy
@property
def is_on(self):
"""Return true if light is on."""
return self._device.state
- @property
- def supported_features(self):
- """Flag supported features."""
- return self._features
-
async def async_turn_on(self, **kwargs):
"""Turn on light."""
data = {"on": True}
+ if ATTR_BRIGHTNESS in kwargs:
+ data["bri"] = kwargs[ATTR_BRIGHTNESS]
+
if ATTR_COLOR_TEMP in kwargs:
data["ct"] = kwargs[ATTR_COLOR_TEMP]
if ATTR_HS_COLOR in kwargs:
- if self._device.xy is not None:
- data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR])
- else:
- data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
- data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
+ data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
+ data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
- if ATTR_BRIGHTNESS in kwargs:
- data["bri"] = kwargs[ATTR_BRIGHTNESS]
+ if ATTR_XY_COLOR in kwargs:
+ data["xy"] = kwargs[ATTR_XY_COLOR]
if ATTR_TRANSITION in kwargs:
data["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10)
@@ -225,7 +240,7 @@ class DeconzBaseLight(DeconzDevice, LightEntity):
@property
def extra_state_attributes(self):
"""Return the device state attributes."""
- return {"is_deconz_group": self._device.type == "LightGroup"}
+ return {DECONZ_GROUP: self._device.type == "LightGroup"}
class DeconzLight(DeconzBaseLight):
@@ -248,14 +263,8 @@ class DeconzGroup(DeconzBaseLight):
def __init__(self, device, gateway):
"""Set up group and create an unique id."""
self._unique_id = f"{gateway.bridgeid}-{device.deconz_id}"
-
super().__init__(device, gateway)
- for light_id in device.lights:
- light = gateway.api.lights[light_id]
- if light.ZHATYPE == Light.ZHATYPE:
- self.update_features(light)
-
@property
def unique_id(self):
"""Return a unique identifier for this device."""
@@ -264,14 +273,12 @@ class DeconzGroup(DeconzBaseLight):
@property
def device_info(self):
"""Return a device description for device registry."""
- bridgeid = self.gateway.api.config.bridgeid
-
return {
"identifiers": {(DECONZ_DOMAIN, self.unique_id)},
"manufacturer": "Dresden Elektronik",
"model": "deCONZ group",
"name": self._device.name,
- "via_device": (DECONZ_DOMAIN, bridgeid),
+ "via_device": (DECONZ_DOMAIN, self.gateway.api.config.bridgeid),
}
@property
diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json
index c4dfd0d4dfc..ad57b1bd903 100644
--- a/homeassistant/components/deconz/manifest.json
+++ b/homeassistant/components/deconz/manifest.json
@@ -3,7 +3,7 @@
"name": "deCONZ",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/deconz",
- "requirements": ["pydeconz==79"],
+ "requirements": ["pydeconz==80"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics"
diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py
index ecd363f121a..f4a4d328d22 100644
--- a/homeassistant/components/deconz/scene.py
+++ b/homeassistant/components/deconz/scene.py
@@ -38,6 +38,8 @@ class DeconzScene(Scene):
self._scene = scene
self.gateway = gateway
+ self._attr_name = scene.full_name
+
async def async_added_to_hass(self):
"""Subscribe to sensors events."""
self.gateway.deconz_ids[self.entity_id] = self._scene.deconz_id
@@ -50,8 +52,3 @@ class DeconzScene(Scene):
async def async_activate(self, **kwargs: Any) -> None:
"""Activate the scene."""
await self._scene.async_set_state({})
-
- @property
- def name(self):
- """Return the name of the scene."""
- return self._scene.full_name
diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py
index bbc49f786ea..e0f12303946 100644
--- a/homeassistant/components/deconz/sensor.py
+++ b/homeassistant/components/deconz/sensor.py
@@ -149,6 +149,15 @@ class DeconzSensor(DeconzDevice, SensorEntity):
TYPE = DOMAIN
+ def __init__(self, device, gateway):
+ """Initialize deCONZ binary sensor."""
+ super().__init__(device, gateway)
+
+ self._attr_device_class = DEVICE_CLASS.get(type(self._device))
+ self._attr_icon = ICON.get(type(self._device))
+ self._attr_state_class = STATE_CLASS.get(type(self._device))
+ self._attr_unit_of_measurement = UNIT_OF_MEASUREMENT.get(type(self._device))
+
@callback
def async_update_callback(self, force_update=False):
"""Update the sensor's state."""
@@ -161,26 +170,6 @@ class DeconzSensor(DeconzDevice, SensorEntity):
"""Return the state of the sensor."""
return self._device.state
- @property
- def device_class(self):
- """Return the class of the sensor."""
- return DEVICE_CLASS.get(type(self._device))
-
- @property
- def icon(self):
- """Return the icon to use in the frontend."""
- return ICON.get(type(self._device))
-
- @property
- def state_class(self):
- """Return the state class of the sensor."""
- return STATE_CLASS.get(type(self._device))
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this sensor."""
- return UNIT_OF_MEASUREMENT.get(type(self._device))
-
@property
def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
@@ -219,8 +208,18 @@ class DeconzTemperature(DeconzDevice, SensorEntity):
Extra temperature sensor on certain Xiaomi devices.
"""
+ _attr_device_class = DEVICE_CLASS_TEMPERATURE
+ _attr_state_class = STATE_CLASS_MEASUREMENT
+ _attr_unit_of_measurement = TEMP_CELSIUS
+
TYPE = DOMAIN
+ def __init__(self, device, gateway):
+ """Initialize deCONZ temperature sensor."""
+ super().__init__(device, gateway)
+
+ self._attr_name = f"{self._device.name} Temperature"
+
@property
def unique_id(self):
"""Return a unique identifier for this device."""
@@ -238,32 +237,22 @@ class DeconzTemperature(DeconzDevice, SensorEntity):
"""Return the state of the sensor."""
return self._device.secondary_temperature
- @property
- def name(self):
- """Return the name of the temperature sensor."""
- return f"{self._device.name} Temperature"
-
- @property
- def device_class(self):
- """Return the class of the sensor."""
- return DEVICE_CLASS_TEMPERATURE
-
- @property
- def state_class(self):
- """Return the state class of the sensor."""
- return STATE_CLASS_MEASUREMENT
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this sensor."""
- return TEMP_CELSIUS
-
class DeconzBattery(DeconzDevice, SensorEntity):
"""Battery class for when a device is only represented as an event."""
+ _attr_device_class = DEVICE_CLASS_BATTERY
+ _attr_state_class = STATE_CLASS_MEASUREMENT
+ _attr_unit_of_measurement = PERCENTAGE
+
TYPE = DOMAIN
+ def __init__(self, device, gateway):
+ """Initialize deCONZ battery level sensor."""
+ super().__init__(device, gateway)
+
+ self._attr_name = f"{self._device.name} Battery Level"
+
@callback
def async_update_callback(self, force_update=False):
"""Update the battery's state, if needed."""
@@ -292,26 +281,6 @@ class DeconzBattery(DeconzDevice, SensorEntity):
"""Return the state of the battery."""
return self._device.battery
- @property
- def name(self):
- """Return the name of the battery."""
- return f"{self._device.name} Battery Level"
-
- @property
- def device_class(self):
- """Return the class of the sensor."""
- return DEVICE_CLASS_BATTERY
-
- @property
- def state_class(self):
- """Return the state class of the sensor."""
- return STATE_CLASS_MEASUREMENT
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity."""
- return PERCENTAGE
-
@property
def extra_state_attributes(self):
"""Return the state attributes of the battery."""
diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py
index d524354ff0b..a4f4aec6a76 100644
--- a/homeassistant/components/deconz/services.py
+++ b/homeassistant/components/deconz/services.py
@@ -59,14 +59,29 @@ async def async_setup_services(hass):
service = service_call.service
service_data = service_call.data
+ gateway = get_master_gateway(hass)
+ if CONF_BRIDGE_ID in service_data:
+ found_gateway = False
+ bridge_id = normalize_bridge_id(service_data[CONF_BRIDGE_ID])
+
+ for possible_gateway in hass.data[DOMAIN].values():
+ if possible_gateway.bridgeid == bridge_id:
+ gateway = possible_gateway
+ found_gateway = True
+ break
+
+ if not found_gateway:
+ LOGGER.error("Could not find the gateway %s", bridge_id)
+ return
+
if service == SERVICE_CONFIGURE_DEVICE:
- await async_configure_service(hass, service_data)
+ await async_configure_service(gateway, service_data)
elif service == SERVICE_DEVICE_REFRESH:
- await async_refresh_devices_service(hass, service_data)
+ await async_refresh_devices_service(gateway)
elif service == SERVICE_REMOVE_ORPHANED_ENTRIES:
- await async_remove_orphaned_entries_service(hass, service_data)
+ await async_remove_orphaned_entries_service(gateway)
hass.services.async_register(
DOMAIN,
@@ -102,7 +117,7 @@ async def async_unload_services(hass):
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_ORPHANED_ENTRIES)
-async def async_configure_service(hass, data):
+async def async_configure_service(gateway, data):
"""Set attribute of device in deCONZ.
Entity is used to resolve to a device path (e.g. '/lights/1').
@@ -118,10 +133,6 @@ async def async_configure_service(hass, data):
See Dresden Elektroniks REST API documentation for details:
http://dresden-elektronik.github.io/deconz-rest-doc/rest/
"""
- gateway = get_master_gateway(hass)
- if CONF_BRIDGE_ID in data:
- gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])]
-
field = data.get(SERVICE_FIELD, "")
entity_id = data.get(SERVICE_ENTITY)
data = data[SERVICE_DATA]
@@ -136,31 +147,21 @@ async def async_configure_service(hass, data):
await gateway.api.request("put", field, json=data)
-async def async_refresh_devices_service(hass, data):
+async def async_refresh_devices_service(gateway):
"""Refresh available devices from deCONZ."""
- gateway = get_master_gateway(hass)
- if CONF_BRIDGE_ID in data:
- gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])]
-
gateway.ignore_state_updates = True
await gateway.api.refresh_state()
gateway.ignore_state_updates = False
- gateway.async_add_device_callback(NEW_GROUP, force=True)
- gateway.async_add_device_callback(NEW_LIGHT, force=True)
- gateway.async_add_device_callback(NEW_SCENE, force=True)
- gateway.async_add_device_callback(NEW_SENSOR, force=True)
+ for new_device_type in [NEW_GROUP, NEW_LIGHT, NEW_SCENE, NEW_SENSOR]:
+ gateway.async_add_device_callback(new_device_type, force=True)
-async def async_remove_orphaned_entries_service(hass, data):
+async def async_remove_orphaned_entries_service(gateway):
"""Remove orphaned deCONZ entries from device and entity registries."""
- gateway = get_master_gateway(hass)
- if CONF_BRIDGE_ID in data:
- gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])]
-
device_registry, entity_registry = await asyncio.gather(
- hass.helpers.device_registry.async_get_registry(),
- hass.helpers.entity_registry.async_get_registry(),
+ gateway.hass.helpers.device_registry.async_get_registry(),
+ gateway.hass.helpers.entity_registry.async_get_registry(),
)
entity_entries = async_entries_for_config_entry(
diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json
index 99d9e8d1e92..a24dbb44ad4 100644
--- a/homeassistant/components/deconz/translations/de.json
+++ b/homeassistant/components/deconz/translations/de.json
@@ -11,7 +11,7 @@
"error": {
"no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden"
},
- "flow_title": "deCONZ Zigbee Gateway",
+ "flow_title": "{host}",
"step": {
"hassio_confirm": {
"description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Supervisor Add-on {addon} bereitgestellt wird?",
diff --git a/homeassistant/components/deconz/translations/he.json b/homeassistant/components/deconz/translations/he.json
index 163cd813dc3..74a8e1ba54b 100644
--- a/homeassistant/components/deconz/translations/he.json
+++ b/homeassistant/components/deconz/translations/he.json
@@ -2,16 +2,38 @@
"config": {
"abort": {
"already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
- "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ"
+ "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea",
+ "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ",
+ "updated_instance": "\u05de\u05d5\u05e4\u05e2 deCONZ \u05e2\u05d5\u05d3\u05db\u05df \u05e2\u05dd \u05db\u05ea\u05d5\u05d1\u05ea \u05de\u05d0\u05e8\u05d7\u05ea \u05d7\u05d3\u05e9\u05d4"
},
"error": {
"no_key": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05d4\u05d9\u05d4 \u05dc\u05e7\u05d1\u05dc \u05de\u05e4\u05ea\u05d7 API"
},
+ "flow_title": "{host}",
"step": {
"link": {
"description": "\u05d1\u05d8\u05dc \u05d0\u05ea \u05e0\u05e2\u05d9\u05dc\u05ea \u05d4\u05de\u05e9\u05e8 deCONZ \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05e2\u05dd Home Assistant.\n\n 1. \u05e2\u05d1\u05d5\u05e8 \u05d0\u05dc \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05de\u05e2\u05e8\u05db\u05ea deCONZ \n .2 \u05dc\u05d7\u05e5 \u05e2\u05dc \"Unlock Gateway\"",
"title": "\u05e7\u05e9\u05e8 \u05e2\u05dd deCONZ"
+ },
+ "manual_input": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u05d1\u05d7\u05e8 \u05e9\u05e2\u05e8 deCONZ \u05e9\u05d4\u05ea\u05d2\u05dc\u05d4"
+ }
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_5": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05d7\u05de\u05d9\u05e9\u05d9",
+ "button_6": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05d9\u05e9\u05d9",
+ "button_7": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05d1\u05d9\u05e2\u05d9",
+ "button_8": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05de\u05d9\u05e0\u05d9"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json
index 29031a7d731..9c83fb806e8 100644
--- a/homeassistant/components/deconz/translations/nl.json
+++ b/homeassistant/components/deconz/translations/nl.json
@@ -3,9 +3,9 @@
"abort": {
"already_configured": "Bridge is al geconfigureerd",
"already_in_progress": "De configuratiestroom is al aan de gang",
- "no_bridges": "Geen deCONZ apparaten ontdekt",
+ "no_bridges": "Geen deCONZ bridges ontdekt",
"no_hardware_available": "Geen radiohardware aangesloten op deCONZ",
- "not_deconz_bridge": "Dit is geen deCONZ bridge",
+ "not_deconz_bridge": "Geen deCONZ bridge",
"updated_instance": "DeCONZ-instantie bijgewerkt met nieuw host-adres"
},
"error": {
@@ -67,7 +67,7 @@
"remote_button_double_press": "\"{subtype}\" knop dubbel geklikt",
"remote_button_long_press": "\" {subtype} \" knop continu ingedrukt",
"remote_button_long_release": "\"{subtype}\" knop losgelaten na lang indrukken van de knop",
- "remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt",
+ "remote_button_quadruple_press": "\" {subtype} \" knop vier keer aangeklikt",
"remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt",
"remote_button_rotated": "Knop gedraaid \" {subtype} \"",
"remote_button_rotated_fast": "Knop is snel gedraaid \" {subtype} \"",
@@ -76,13 +76,13 @@
"remote_button_short_release": "\"{subtype}\" knop losgelaten",
"remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt",
"remote_double_tap": "Apparaat \"{subtype}\" dubbel getikt",
- "remote_double_tap_any_side": "Apparaat dubbel getikt aan elke kant",
+ "remote_double_tap_any_side": "Apparaat dubbel getikt op willekeurige zijde",
"remote_falling": "Apparaat in vrije val",
- "remote_flip_180_degrees": "Apparaat 180 graden omgedraaid",
- "remote_flip_90_degrees": "Apparaat 90 graden omgedraaid",
+ "remote_flip_180_degrees": "Apparaat 180 graden gedraaid",
+ "remote_flip_90_degrees": "Apparaat 90 graden gedraaid",
"remote_gyro_activated": "Apparaat geschud",
"remote_moved": "Apparaat verplaatst met \"{subtype}\" omhoog",
- "remote_moved_any_side": "Apparaat gedraaid met elke kant naar boven",
+ "remote_moved_any_side": "Apparaat gedraaid met willekeurige zijde boven",
"remote_rotate_from_side_1": "Apparaat gedraaid van \"zijde 1\" naar \"{subtype}\"\".",
"remote_rotate_from_side_2": "Apparaat gedraaid van \"zijde 2\" naar \"{subtype}\"\".",
"remote_rotate_from_side_3": "Apparaat gedraaid van \"zijde 3\" naar \" {subtype} \"",
diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py
index cff93c89954..b105ff5ff7b 100644
--- a/homeassistant/components/delijn/sensor.py
+++ b/homeassistant/components/delijn/sensor.py
@@ -60,6 +60,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class DeLijnPublicTransportSensor(SensorEntity):
"""Representation of a Ruter sensor."""
+ _attr_device_class = DEVICE_CLASS_TIMESTAMP
+
def __init__(self, line):
"""Initialize the sensor."""
self.line = line
@@ -104,11 +106,6 @@ class DeLijnPublicTransportSensor(SensorEntity):
"""Return True if entity is available."""
return self._available
- @property
- def device_class(self):
- """Return the device class."""
- return DEVICE_CLASS_TIMESTAMP
-
@property
def name(self):
"""Return the name of the sensor."""
diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py
index b32537ae44e..acd98465207 100644
--- a/homeassistant/components/demo/__init__.py
+++ b/homeassistant/components/demo/__init__.py
@@ -20,6 +20,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
"lock",
"media_player",
"number",
+ "select",
"sensor",
"switch",
"vacuum",
diff --git a/homeassistant/components/demo/humidifier.py b/homeassistant/components/demo/humidifier.py
index 35eb6e18537..7ee5c0fc6ef 100644
--- a/homeassistant/components/demo/humidifier.py
+++ b/homeassistant/components/demo/humidifier.py
@@ -1,4 +1,6 @@
"""Demo platform that offers a fake humidifier device."""
+from __future__ import annotations
+
from homeassistant.components.humidifier import HumidifierEntity
from homeassistant.components.humidifier.const import (
DEVICE_CLASS_DEHUMIDIFIER,
@@ -43,82 +45,46 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class DemoHumidifier(HumidifierEntity):
"""Representation of a demo humidifier device."""
+ _attr_should_poll = False
+
def __init__(
self,
- name,
- mode,
- target_humidity,
- available_modes=None,
- is_on=True,
- device_class=None,
- ):
+ name: str,
+ mode: str | None,
+ target_humidity: int,
+ available_modes: list[str] | None = None,
+ is_on: bool = True,
+ device_class: str | None = None,
+ ) -> None:
"""Initialize the humidifier device."""
- self._name = name
- self._state = is_on
- self._support_flags = SUPPORT_FLAGS
+ self._attr_name = name
+ self._attr_is_on = is_on
+ self._attr_supported_features = SUPPORT_FLAGS
if mode is not None:
- self._support_flags = self._support_flags | SUPPORT_MODES
- self._target_humidity = target_humidity
- self._mode = mode
- self._available_modes = available_modes
- self._device_class = device_class
-
- @property
- def supported_features(self):
- """Return the list of supported features."""
- return self._support_flags
-
- @property
- def should_poll(self):
- """Return the polling state."""
- return False
-
- @property
- def name(self):
- """Return the name of the humidity device."""
- return self._name
-
- @property
- def target_humidity(self):
- """Return the humidity we try to reach."""
- return self._target_humidity
-
- @property
- def mode(self):
- """Return current mode."""
- return self._mode
-
- @property
- def available_modes(self):
- """Return available modes."""
- return self._available_modes
-
- @property
- def is_on(self):
- """Return true if the humidifier is on."""
- return self._state
-
- @property
- def device_class(self):
- """Return the device class of the humidifier."""
- return self._device_class
+ self._attr_supported_features = (
+ self._attr_supported_features | SUPPORT_MODES
+ )
+ self._attr_target_humidity = target_humidity
+ self._attr_mode = mode
+ self._attr_available_modes = available_modes
+ self._attr_device_class = device_class
async def async_turn_on(self, **kwargs):
"""Turn the device on."""
- self._state = True
+ self._attr_is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""Turn the device off."""
- self._state = False
+ self._attr_is_on = False
self.async_write_ha_state()
async def async_set_humidity(self, humidity):
"""Set new humidity level."""
- self._target_humidity = humidity
+ self._attr_target_humidity = humidity
self.async_write_ha_state()
async def async_set_mode(self, mode):
"""Update mode."""
- self._mode = mode
+ self._attr_mode = mode
self.async_write_ha_state()
diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py
index 6680cd23874..4d9496ca10f 100644
--- a/homeassistant/components/demo/light.py
+++ b/homeassistant/components/demo/light.py
@@ -10,10 +10,12 @@ from homeassistant.components.light import (
ATTR_HS_COLOR,
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
+ ATTR_WHITE,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_HS,
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
+ COLOR_MODE_WHITE,
SUPPORT_EFFECT,
LightEntity,
)
@@ -27,6 +29,7 @@ LIGHT_EFFECT_LIST = ["rainbow", "none"]
LIGHT_TEMPS = [240, 380]
SUPPORT_DEMO = {COLOR_MODE_HS, COLOR_MODE_COLOR_TEMP}
+SUPPORT_DEMO_HS_WHITE = {COLOR_MODE_HS, COLOR_MODE_WHITE}
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -72,6 +75,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
supported_color_modes={COLOR_MODE_RGBWW},
unique_id="light_5",
),
+ DemoLight(
+ available=True,
+ name="Entrance Color + White Lights",
+ hs_color=LIGHT_COLORS[1],
+ state=True,
+ supported_color_modes=SUPPORT_DEMO_HS_WHITE,
+ unique_id="light_6",
+ ),
]
)
@@ -218,6 +229,20 @@ class DemoLight(LightEntity):
"""Turn the light on."""
self._state = True
+ if ATTR_BRIGHTNESS in kwargs:
+ self._brightness = kwargs[ATTR_BRIGHTNESS]
+
+ if ATTR_COLOR_TEMP in kwargs:
+ self._color_mode = COLOR_MODE_COLOR_TEMP
+ self._ct = kwargs[ATTR_COLOR_TEMP]
+
+ if ATTR_EFFECT in kwargs:
+ self._effect = kwargs[ATTR_EFFECT]
+
+ if ATTR_HS_COLOR in kwargs:
+ self._color_mode = COLOR_MODE_HS
+ self._hs_color = kwargs[ATTR_HS_COLOR]
+
if ATTR_RGBW_COLOR in kwargs:
self._color_mode = COLOR_MODE_RGBW
self._rgbw_color = kwargs[ATTR_RGBW_COLOR]
@@ -226,19 +251,9 @@ class DemoLight(LightEntity):
self._color_mode = COLOR_MODE_RGBWW
self._rgbww_color = kwargs[ATTR_RGBWW_COLOR]
- if ATTR_HS_COLOR in kwargs:
- self._color_mode = COLOR_MODE_HS
- self._hs_color = kwargs[ATTR_HS_COLOR]
-
- if ATTR_COLOR_TEMP in kwargs:
- self._color_mode = COLOR_MODE_COLOR_TEMP
- self._ct = kwargs[ATTR_COLOR_TEMP]
-
- if ATTR_BRIGHTNESS in kwargs:
- self._brightness = kwargs[ATTR_BRIGHTNESS]
-
- if ATTR_EFFECT in kwargs:
- self._effect = kwargs[ATTR_EFFECT]
+ if ATTR_WHITE in kwargs:
+ self._color_mode = COLOR_MODE_WHITE
+ self._brightness = kwargs[ATTR_WHITE]
# As we have disabled polling, we need to inform
# Home Assistant about updates in our state ourselves.
diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py
index 63f2d218957..cafc0e3f748 100644
--- a/homeassistant/components/demo/lock.py
+++ b/homeassistant/components/demo/lock.py
@@ -22,44 +22,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class DemoLock(LockEntity):
"""Representation of a Demo lock."""
- def __init__(self, name, state, openable=False):
+ _attr_should_poll = False
+
+ def __init__(self, name: str, state: str, openable: bool = False) -> None:
"""Initialize the lock."""
- self._name = name
- self._state = state
- self._openable = openable
-
- @property
- def should_poll(self):
- """No polling needed for a demo lock."""
- return False
-
- @property
- def name(self):
- """Return the name of the lock if any."""
- return self._name
-
- @property
- def is_locked(self):
- """Return true if lock is locked."""
- return self._state == STATE_LOCKED
+ self._attr_name = name
+ self._attr_is_locked = state == STATE_LOCKED
+ if openable:
+ self._attr_supported_features = SUPPORT_OPEN
def lock(self, **kwargs):
"""Lock the device."""
- self._state = STATE_LOCKED
+ self._attr_is_locked = True
self.schedule_update_ha_state()
def unlock(self, **kwargs):
"""Unlock the device."""
- self._state = STATE_UNLOCKED
+ self._attr_is_locked = False
self.schedule_update_ha_state()
def open(self, **kwargs):
"""Open the door latch."""
- self._state = STATE_UNLOCKED
+ self._attr_is_locked = False
self.schedule_update_ha_state()
-
- @property
- def supported_features(self):
- """Flag supported features."""
- if self._openable:
- return SUPPORT_OPEN
diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py
index f3fd815f621..a6842d2ca43 100644
--- a/homeassistant/components/demo/number.py
+++ b/homeassistant/components/demo/number.py
@@ -1,4 +1,6 @@
"""Demo platform that offers a fake Number entity."""
+from __future__ import annotations
+
import voluptuous as vol
from homeassistant.components.number import NumberEntity
@@ -40,26 +42,32 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class DemoNumber(NumberEntity):
"""Representation of a demo Number entity."""
+ _attr_should_poll = False
+
def __init__(
self,
- unique_id,
- name,
- state,
- icon,
- assumed,
- min_value=None,
- max_value=None,
+ unique_id: str,
+ name: str,
+ state: float,
+ icon: str,
+ assumed: bool,
+ min_value: float | None = None,
+ max_value: float | None = None,
step=None,
- ):
+ ) -> None:
"""Initialize the Demo Number entity."""
- self._unique_id = unique_id
- self._name = name or DEVICE_DEFAULT_NAME
- self._state = state
- self._icon = icon
- self._assumed = assumed
- self._min_value = min_value
- self._max_value = max_value
- self._step = step
+ self._attr_assumed_state = assumed
+ self._attr_icon = icon
+ self._attr_name = name or DEVICE_DEFAULT_NAME
+ self._attr_unique_id = unique_id
+ self._attr_value = state
+
+ if min_value is not None:
+ self._attr_min_value = min_value
+ if max_value is not None:
+ self._attr_max_value = max_value
+ if step is not None:
+ self._attr_step = step
@property
def device_info(self):
@@ -72,51 +80,6 @@ class DemoNumber(NumberEntity):
"name": self.name,
}
- @property
- def unique_id(self):
- """Return the unique id."""
- return self._unique_id
-
- @property
- def should_poll(self):
- """No polling needed for a demo Number entity."""
- return False
-
- @property
- def name(self):
- """Return the name of the device if any."""
- return self._name
-
- @property
- def icon(self):
- """Return the icon to use for device if any."""
- return self._icon
-
- @property
- def assumed_state(self):
- """Return if the state is based on assumptions."""
- return self._assumed
-
- @property
- def value(self):
- """Return the current value."""
- return self._state
-
- @property
- def min_value(self):
- """Return the minimum value."""
- return self._min_value or super().min_value
-
- @property
- def max_value(self):
- """Return the maximum value."""
- return self._max_value or super().max_value
-
- @property
- def step(self):
- """Return the value step."""
- return self._step or super().step
-
async def async_set_value(self, value):
"""Update the current value."""
num_value = float(value)
@@ -126,5 +89,5 @@ class DemoNumber(NumberEntity):
f"Invalid value for {self.entity_id}: {value} (range {self.min_value} - {self.max_value})"
)
- self._state = num_value
+ self._attr_value = num_value
self.async_write_ha_state()
diff --git a/homeassistant/components/demo/remote.py b/homeassistant/components/demo/remote.py
index 98e949f38c3..1badd391575 100644
--- a/homeassistant/components/demo/remote.py
+++ b/homeassistant/components/demo/remote.py
@@ -1,14 +1,32 @@
"""Demo platform that has two fake remotes."""
+from __future__ import annotations
+
+from collections.abc import Iterable
+from typing import Any
+
from homeassistant.components.remote import RemoteEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DEVICE_DEFAULT_NAME
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType
-async def async_setup_entry(hass, config_entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
"""Set up the Demo config entry."""
setup_platform(hass, {}, async_add_entities)
-def setup_platform(hass, config, add_entities_callback, discovery_info=None):
+def setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ add_entities_callback: AddEntitiesCallback,
+ discovery_info: dict[str, Any] | None = None,
+) -> None:
"""Set up the demo remotes."""
add_entities_callback(
[
@@ -21,50 +39,32 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None):
class DemoRemote(RemoteEntity):
"""Representation of a demo remote."""
- def __init__(self, name, state, icon):
+ _attr_should_poll = False
+
+ def __init__(self, name: str | None, state: bool, icon: str | None) -> None:
"""Initialize the Demo Remote."""
- self._name = name or DEVICE_DEFAULT_NAME
- self._state = state
- self._icon = icon
+ self._attr_name = name or DEVICE_DEFAULT_NAME
+ self._attr_is_on = state
+ self._attr_icon = icon
self._last_command_sent = None
@property
- def should_poll(self):
- """No polling needed for a demo remote."""
- return False
-
- @property
- def name(self):
- """Return the name of the device if any."""
- return self._name
-
- @property
- def icon(self):
- """Return the icon to use for device if any."""
- return self._icon
-
- @property
- def is_on(self):
- """Return true if remote is on."""
- return self._state
-
- @property
- def extra_state_attributes(self):
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return device state attributes."""
if self._last_command_sent is not None:
return {"last_command_sent": self._last_command_sent}
- def turn_on(self, **kwargs):
+ def turn_on(self, **kwargs: Any) -> None:
"""Turn the remote on."""
- self._state = True
+ self._attr_is_on = True
self.schedule_update_ha_state()
- def turn_off(self, **kwargs):
+ def turn_off(self, **kwargs: Any) -> None:
"""Turn the remote off."""
- self._state = False
+ self._attr_is_on = False
self.schedule_update_ha_state()
- def send_command(self, command, **kwargs):
+ def send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a command to a device."""
for com in command:
self._last_command_sent = com
diff --git a/homeassistant/components/demo/select.py b/homeassistant/components/demo/select.py
new file mode 100644
index 00000000000..dcc0c12a9b4
--- /dev/null
+++ b/homeassistant/components/demo/select.py
@@ -0,0 +1,80 @@
+"""Demo platform that offers a fake select entity."""
+from __future__ import annotations
+
+from homeassistant.components.select import SelectEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import DEVICE_DEFAULT_NAME
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+
+from . import DOMAIN
+
+
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType = None,
+) -> None:
+ """Set up the demo Select entity."""
+ async_add_entities(
+ [
+ DemoSelect(
+ unique_id="speed",
+ name="Speed",
+ icon="mdi:speedometer",
+ device_class="demo__speed",
+ current_option="ridiculous_speed",
+ options=[
+ "light_speed",
+ "ridiculous_speed",
+ "ludicrous_speed",
+ ],
+ ),
+ ]
+ )
+
+
+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)
+
+
+class DemoSelect(SelectEntity):
+ """Representation of a demo select entity."""
+
+ _attr_should_poll = False
+
+ def __init__(
+ self,
+ unique_id: str,
+ name: str,
+ icon: str,
+ device_class: str | None,
+ current_option: str | None,
+ options: list[str],
+ ) -> None:
+ """Initialize the Demo select entity."""
+ self._attr_unique_id = unique_id
+ self._attr_name = name or DEVICE_DEFAULT_NAME
+ self._attr_current_option = current_option
+ self._attr_icon = icon
+ self._attr_device_class = device_class
+ self._attr_options = options
+ self._attr_device_info = {
+ "identifiers": {(DOMAIN, unique_id)},
+ "name": name,
+ }
+
+ async def async_select_option(self, option: str) -> None:
+ """Update the current selected option."""
+ if option not in self.options:
+ raise ValueError(f"Invalid option for {self.entity_id}: {option}")
+
+ self._attr_current_option = option
+ self.async_write_ha_state()
diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py
index 00111d34dd4..817cbd435d1 100644
--- a/homeassistant/components/demo/sensor.py
+++ b/homeassistant/components/demo/sensor.py
@@ -1,5 +1,10 @@
"""Demo platform that has a couple of fake sensors."""
+from __future__ import annotations
+
+from typing import Any
+
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
CONCENTRATION_PARTS_PER_MILLION,
@@ -10,11 +15,19 @@ from homeassistant.const import (
PERCENTAGE,
TEMP_CELSIUS,
)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, StateType
from . import DOMAIN
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: AddEntitiesCallback,
+ discovery_info: dict[str, Any] | None = None,
+) -> None:
"""Set up the Demo sensors."""
async_add_entities(
[
@@ -58,7 +71,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-async def async_setup_entry(hass, config_entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
"""Set up the Demo config entry."""
await async_setup_platform(hass, {}, async_add_entities)
@@ -66,73 +83,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class DemoSensor(SensorEntity):
"""Representation of a Demo sensor."""
+ _attr_should_poll = False
+
def __init__(
self,
- unique_id,
- name,
- state,
- device_class,
- state_class,
- unit_of_measurement,
- battery,
- ):
+ unique_id: str,
+ name: str,
+ state: StateType,
+ device_class: str | None,
+ state_class: str | None,
+ unit_of_measurement: str | None,
+ battery: StateType,
+ ) -> None:
"""Initialize the sensor."""
- self._battery = battery
- self._device_class = device_class
- self._name = name
- self._state = state
- self._state_class = state_class
- self._unique_id = unique_id
- self._unit_of_measurement = unit_of_measurement
+ self._attr_device_class = device_class
+ self._attr_name = name
+ self._attr_state = state
+ self._attr_state_class = state_class
+ self._attr_unique_id = unique_id
+ self._attr_unit_of_measurement = unit_of_measurement
- @property
- def device_info(self):
- """Return device info."""
- return {
- "identifiers": {
- # Serial numbers are unique identifiers within a specific domain
- (DOMAIN, self.unique_id)
- },
- "name": self.name,
+ self._attr_device_info = {
+ "identifiers": {(DOMAIN, unique_id)},
+ "name": name,
}
- @property
- def unique_id(self):
- """Return the unique id."""
- return self._unique_id
-
- @property
- def should_poll(self):
- """No polling needed for a demo sensor."""
- return False
-
- @property
- def device_class(self):
- """Return the device class of the sensor."""
- return self._device_class
-
- @property
- def state_class(self):
- """Return the state class of the sensor."""
- return self._state_class
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def unit_of_measurement(self):
- """Return the unit this state is expressed in."""
- return self._unit_of_measurement
-
- @property
- def extra_state_attributes(self):
- """Return the state attributes."""
- if self._battery:
- return {ATTR_BATTERY_LEVEL: self._battery}
+ if battery:
+ self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: battery}
diff --git a/homeassistant/components/demo/strings.select.json b/homeassistant/components/demo/strings.select.json
new file mode 100644
index 00000000000..f797ab562bc
--- /dev/null
+++ b/homeassistant/components/demo/strings.select.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "demo__speed": {
+ "light_speed": "Light Speed",
+ "ludicrous_speed": "Ludicrous Speed",
+ "ridiculous_speed": "Ridiculous Speed"
+ }
+ }
+}
diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py
index cdbeb142677..13853959a12 100644
--- a/homeassistant/components/demo/switch.py
+++ b/homeassistant/components/demo/switch.py
@@ -1,4 +1,6 @@
"""Demo platform that has two fake switches."""
+from __future__ import annotations
+
from homeassistant.components.switch import SwitchEntity
from homeassistant.const import DEVICE_DEFAULT_NAME
@@ -9,9 +11,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"""Set up the demo switches."""
async_add_entities(
[
- DemoSwitch("swith1", "Decorative Lights", True, None, True),
+ DemoSwitch("switch1", "Decorative Lights", True, None, True),
DemoSwitch(
- "swith2",
+ "switch2",
"AC",
False,
"mdi:air-conditioner",
@@ -30,78 +32,42 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class DemoSwitch(SwitchEntity):
"""Representation of a demo switch."""
- def __init__(self, unique_id, name, state, icon, assumed, device_class=None):
+ _attr_should_poll = False
+
+ def __init__(
+ self,
+ unique_id: str,
+ name: str,
+ state: bool,
+ icon: str | None,
+ assumed: bool,
+ device_class: str | None = None,
+ ) -> None:
"""Initialize the Demo switch."""
- self._unique_id = unique_id
- self._name = name or DEVICE_DEFAULT_NAME
- self._state = state
- self._icon = icon
- self._assumed = assumed
- self._device_class = device_class
+ self._attr_assumed_state = assumed
+ self._attr_device_class = device_class
+ self._attr_icon = icon
+ self._attr_is_on = state
+ self._attr_name = name or DEVICE_DEFAULT_NAME
+ self._attr_today_energy_kwh = 15
+ self._attr_unique_id = unique_id
@property
def device_info(self):
"""Return device info."""
return {
- "identifiers": {
- # Serial numbers are unique identifiers within a specific domain
- (DOMAIN, self.unique_id)
- },
+ "identifiers": {(DOMAIN, self.unique_id)},
"name": self.name,
}
- @property
- def unique_id(self):
- """Return the unique id."""
- return self._unique_id
-
- @property
- def should_poll(self):
- """No polling needed for a demo switch."""
- return False
-
- @property
- def name(self):
- """Return the name of the device if any."""
- return self._name
-
- @property
- def icon(self):
- """Return the icon to use for device if any."""
- return self._icon
-
- @property
- def assumed_state(self):
- """Return if the state is based on assumptions."""
- return self._assumed
-
- @property
- def current_power_w(self):
- """Return the current power usage in W."""
- if self._state:
- return 100
-
- @property
- def today_energy_kwh(self):
- """Return the today total energy usage in kWh."""
- return 15
-
- @property
- def is_on(self):
- """Return true if switch is on."""
- return self._state
-
- @property
- def device_class(self):
- """Return device of entity."""
- return self._device_class
-
def turn_on(self, **kwargs):
"""Turn the switch on."""
- self._state = True
+ self._attr_is_on = True
+ self._attr_current_power_w = 100
self.schedule_update_ha_state()
def turn_off(self, **kwargs):
"""Turn the device off."""
- self._state = False
+ self._attr_is_on = False
+ self._attr_current_power_w = 0
self.schedule_update_ha_state()
diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json
index dc3e218895b..27a068a1918 100644
--- a/homeassistant/components/demo/translations/it.json
+++ b/homeassistant/components/demo/translations/it.json
@@ -1,6 +1,12 @@
{
"options": {
"step": {
+ "init": {
+ "data": {
+ "one": "Pi\u00f9",
+ "other": "Altri"
+ }
+ },
"options_1": {
"data": {
"bool": "Valore booleano facoltativo",
diff --git a/homeassistant/components/demo/translations/select.ca.json b/homeassistant/components/demo/translations/select.ca.json
new file mode 100644
index 00000000000..c66c285ffda
--- /dev/null
+++ b/homeassistant/components/demo/translations/select.ca.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "demo__speed": {
+ "light_speed": "Velocitat de la llum",
+ "ludicrous_speed": "Velocitat Ludicrous",
+ "ridiculous_speed": "Velocitat rid\u00edcula"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/translations/select.de.json b/homeassistant/components/demo/translations/select.de.json
new file mode 100644
index 00000000000..2d801f47c13
--- /dev/null
+++ b/homeassistant/components/demo/translations/select.de.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "demo__speed": {
+ "light_speed": "Lichtgeschwindigkeit",
+ "ludicrous_speed": "Wahnsinnige Geschwindigkeit",
+ "ridiculous_speed": "L\u00e4cherliche Geschwindigkeit"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/translations/select.en.json b/homeassistant/components/demo/translations/select.en.json
new file mode 100644
index 00000000000..e7f7c67f452
--- /dev/null
+++ b/homeassistant/components/demo/translations/select.en.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "demo__speed": {
+ "light_speed": "Light Speed",
+ "ludicrous_speed": "Ludicrous Speed",
+ "ridiculous_speed": "Ridiculous Speed"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/translations/select.es.json b/homeassistant/components/demo/translations/select.es.json
new file mode 100644
index 00000000000..b2a20fdcfec
--- /dev/null
+++ b/homeassistant/components/demo/translations/select.es.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "demo__speed": {
+ "light_speed": "Velocidad de la luz",
+ "ludicrous_speed": "Velocidad rid\u00edcula",
+ "ridiculous_speed": "Velocidad rid\u00edcula"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/translations/select.et.json b/homeassistant/components/demo/translations/select.et.json
new file mode 100644
index 00000000000..eee34b646cc
--- /dev/null
+++ b/homeassistant/components/demo/translations/select.et.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "demo__speed": {
+ "light_speed": "Valguse kiirus",
+ "ludicrous_speed": "Meeletu kiirus",
+ "ridiculous_speed": "Naeruv\u00e4\u00e4rne kiirus"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/translations/select.hu.json b/homeassistant/components/demo/translations/select.hu.json
new file mode 100644
index 00000000000..4afeff7b1d3
--- /dev/null
+++ b/homeassistant/components/demo/translations/select.hu.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "demo__speed": {
+ "light_speed": "F\u00e9nysebess\u00e9g",
+ "ludicrous_speed": "Hihetetlen sebess\u00e9g",
+ "ridiculous_speed": "K\u00e9ptelen sebess\u00e9g"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/translations/select.it.json b/homeassistant/components/demo/translations/select.it.json
new file mode 100644
index 00000000000..ba49e1ab60a
--- /dev/null
+++ b/homeassistant/components/demo/translations/select.it.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "demo__speed": {
+ "light_speed": "Velocit\u00e0 della luce",
+ "ludicrous_speed": "Velocit\u00e0 comica",
+ "ridiculous_speed": "Velocit\u00e0 ridicola"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/translations/select.no.json b/homeassistant/components/demo/translations/select.no.json
new file mode 100644
index 00000000000..246195bfd26
--- /dev/null
+++ b/homeassistant/components/demo/translations/select.no.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "demo__speed": {
+ "light_speed": "Lyshastighet",
+ "ludicrous_speed": "Ludicrous Speed",
+ "ridiculous_speed": "Latterlig hastighet"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/translations/select.pl.json b/homeassistant/components/demo/translations/select.pl.json
new file mode 100644
index 00000000000..e90b2ccd0cb
--- /dev/null
+++ b/homeassistant/components/demo/translations/select.pl.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "demo__speed": {
+ "light_speed": "Pr\u0119dko\u015b\u0107 \u015bwiat\u0142a",
+ "ludicrous_speed": "Absurdalna pr\u0119dko\u015b\u0107",
+ "ridiculous_speed": "Niewiarygodna pr\u0119dko\u015b\u0107"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/translations/select.ru.json b/homeassistant/components/demo/translations/select.ru.json
new file mode 100644
index 00000000000..0de63078eeb
--- /dev/null
+++ b/homeassistant/components/demo/translations/select.ru.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "demo__speed": {
+ "light_speed": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u0441\u0432\u0435\u0442\u0430",
+ "ludicrous_speed": "\u0427\u0443\u0434\u043e\u0432\u0438\u0449\u043d\u0430\u044f \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c",
+ "ridiculous_speed": "\u041d\u0435\u0432\u0435\u0440\u043e\u044f\u0442\u043d\u0430\u044f \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/translations/select.zh-Hant.json b/homeassistant/components/demo/translations/select.zh-Hant.json
new file mode 100644
index 00000000000..6190837b1f1
--- /dev/null
+++ b/homeassistant/components/demo/translations/select.zh-Hant.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "demo__speed": {
+ "light_speed": "\u5149\u901f",
+ "ludicrous_speed": "\u53ef\u7b11\u7684\u901f\u5ea6",
+ "ridiculous_speed": "\u8352\u8b2c\u7684\u901f\u5ea6"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py
index 0b96bbf75f8..2311f0f457b 100644
--- a/homeassistant/components/demo/water_heater.py
+++ b/homeassistant/components/demo/water_heater.py
@@ -30,23 +30,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class DemoWaterHeater(WaterHeaterEntity):
"""Representation of a demo water_heater device."""
+ _attr_should_poll = False
+ _attr_supported_features = SUPPORT_FLAGS_HEATER
+
def __init__(
self, name, target_temperature, unit_of_measurement, away, current_operation
):
"""Initialize the water_heater device."""
- self._name = name
- self._support_flags = SUPPORT_FLAGS_HEATER
+ self._attr_name = name
if target_temperature is not None:
- self._support_flags = self._support_flags | SUPPORT_TARGET_TEMPERATURE
+ self._attr_supported_features = (
+ self.supported_features | SUPPORT_TARGET_TEMPERATURE
+ )
if away is not None:
- self._support_flags = self._support_flags | SUPPORT_AWAY_MODE
+ self._attr_supported_features = self.supported_features | SUPPORT_AWAY_MODE
if current_operation is not None:
- self._support_flags = self._support_flags | SUPPORT_OPERATION_MODE
- self._target_temperature = target_temperature
- self._unit_of_measurement = unit_of_measurement
- self._away = away
- self._current_operation = current_operation
- self._operation_list = [
+ self._attr_supported_features = (
+ self.supported_features | SUPPORT_OPERATION_MODE
+ )
+ self._attr_target_temperature = target_temperature
+ self._attr_temperature_unit = unit_of_measurement
+ self._attr_is_away_mode_on = away
+ self._attr_current_operation = current_operation
+ self._attr_operation_list = [
"eco",
"electric",
"performance",
@@ -56,62 +62,22 @@ class DemoWaterHeater(WaterHeaterEntity):
"off",
]
- @property
- def supported_features(self):
- """Return the list of supported features."""
- return self._support_flags
-
- @property
- def should_poll(self):
- """Return the polling state."""
- return False
-
- @property
- def name(self):
- """Return the name of the water_heater device."""
- return self._name
-
- @property
- def temperature_unit(self):
- """Return the unit of measurement."""
- return self._unit_of_measurement
-
- @property
- def target_temperature(self):
- """Return the temperature we try to reach."""
- return self._target_temperature
-
- @property
- def current_operation(self):
- """Return current operation ie. heat, cool, idle."""
- return self._current_operation
-
- @property
- def operation_list(self):
- """Return the list of available operation modes."""
- return self._operation_list
-
- @property
- def is_away_mode_on(self):
- """Return if away mode is on."""
- return self._away
-
def set_temperature(self, **kwargs):
"""Set new target temperatures."""
- self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
+ self._attr_target_temperature = kwargs.get(ATTR_TEMPERATURE)
self.schedule_update_ha_state()
def set_operation_mode(self, operation_mode):
"""Set new operation mode."""
- self._current_operation = operation_mode
+ self._attr_current_operation = operation_mode
self.schedule_update_ha_state()
def turn_away_mode_on(self):
"""Turn away mode on."""
- self._away = True
+ self._attr_is_away_mode_on = True
self.schedule_update_ha_state()
def turn_away_mode_off(self):
"""Turn away mode off."""
- self._away = False
+ self._attr_is_away_mode_on = False
self.schedule_update_ha_state()
diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json
index f8896e5a08f..f40c665489c 100644
--- a/homeassistant/components/denonavr/translations/de.json
+++ b/homeassistant/components/denonavr/translations/de.json
@@ -10,7 +10,7 @@
"error": {
"discovery_error": "Denon AVR-Netzwerk-Receiver konnte nicht gefunden werden"
},
- "flow_title": "Denon AVR-Netzwerk-Receiver: {name}",
+ "flow_title": "{name}",
"step": {
"confirm": {
"description": "Bitte best\u00e4tige das Hinzuf\u00fcgen des Receivers",
diff --git a/homeassistant/components/denonavr/translations/he.json b/homeassistant/components/denonavr/translations/he.json
new file mode 100644
index 00000000000..3d080ab97dc
--- /dev/null
+++ b/homeassistant/components/denonavr/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "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"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "select": {
+ "data": {
+ "select_host": "\u05db\u05ea\u05d5\u05d1\u05ea IP \u05e9\u05dc \u05de\u05e7\u05dc\u05d8"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json
index 41a1910bd56..2ae4bc69b55 100644
--- a/homeassistant/components/denonavr/translations/hu.json
+++ b/homeassistant/components/denonavr/translations/hu.json
@@ -8,6 +8,7 @@
"error": {
"discovery_error": "Nem siker\u00fclt megtal\u00e1lni a Denon AVR h\u00e1l\u00f3zati er\u0151s\u00edt\u0151t"
},
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py
index 12083a8d139..93b0b9a4a9d 100644
--- a/homeassistant/components/device_automation/__init__.py
+++ b/homeassistant/components/device_automation/__init__.py
@@ -25,7 +25,7 @@ from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig
DOMAIN = "device_automation"
-TRIGGER_BASE_SCHEMA = vol.Schema(
+DEVICE_TRIGGER_BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "device",
vol.Required(CONF_DOMAIN): str,
diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py
index 72fcc9790b2..cf41fc93d83 100644
--- a/homeassistant/components/device_automation/toggle_entity.py
+++ b/homeassistant/components/device_automation/toggle_entity.py
@@ -29,7 +29,7 @@ from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
-from . import TRIGGER_BASE_SCHEMA
+from . import DEVICE_TRIGGER_BASE_SCHEMA
# mypy: allow-untyped-calls, allow-untyped-defs
@@ -91,7 +91,7 @@ CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
}
)
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]),
diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py
index b2892d1abaa..a1b6e53c5c3 100644
--- a/homeassistant/components/device_automation/trigger.py
+++ b/homeassistant/components/device_automation/trigger.py
@@ -2,14 +2,14 @@
import voluptuous as vol
from homeassistant.components.device_automation import (
- TRIGGER_BASE_SCHEMA,
+ DEVICE_TRIGGER_BASE_SCHEMA,
async_get_device_automation_platform,
)
from homeassistant.const import CONF_DOMAIN
# mypy: allow-untyped-defs, no-check-untyped-defs
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
async def async_validate_trigger_config(hass, config):
diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py
index 0260a4bbd3a..714d6d7f016 100644
--- a/homeassistant/components/device_tracker/device_condition.py
+++ b/homeassistant/components/device_tracker/device_condition.py
@@ -43,24 +43,14 @@ async def async_get_conditions(
continue
# Add conditions for each entity that belongs to this integration
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_home",
- }
- )
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_not_home",
- }
- )
+ base_condition = {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
+ conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES]
return conditions
diff --git a/homeassistant/components/device_tracker/device_trigger.py b/homeassistant/components/device_tracker/device_trigger.py
index 3c7f9ac35ad..e73b5a70075 100644
--- a/homeassistant/components/device_tracker/device_trigger.py
+++ b/homeassistant/components/device_tracker/device_trigger.py
@@ -6,7 +6,7 @@ from typing import Final
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.zone import DOMAIN as DOMAIN_ZONE, trigger as zone
from homeassistant.const import (
CONF_DEVICE_ID,
@@ -25,7 +25,7 @@ from . import DOMAIN
TRIGGER_TYPES: Final[set[str]] = {"enters", "leaves"}
-TRIGGER_SCHEMA: Final = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py
index ded30d75de9..4c8757e4eff 100644
--- a/homeassistant/components/devolo_home_control/__init__.py
+++ b/homeassistant/components/devolo_home_control/__init__.py
@@ -10,7 +10,7 @@ from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import (
CONF_MYDEVOLO,
@@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid)
if not credentials_valid:
- return False
+ raise ConfigEntryAuthFailed
if await hass.async_add_executor_job(mydevolo.maintenance):
raise ConfigEntryNotReady
diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py
index 4f605baf98d..10172b94452 100644
--- a/homeassistant/components/devolo_home_control/config_flow.py
+++ b/homeassistant/components/devolo_home_control/config_flow.py
@@ -8,7 +8,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType
from . import configure_mydevolo
from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, DOMAIN, SUPPORTED_MODEL_TYPES
-from .exceptions import CredentialsInvalid
+from .exceptions import CredentialsInvalid, UuidChanged
class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@@ -22,13 +22,13 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
+ self._reauth_entry = None
+ self._url = DEFAULT_MYDEVOLO
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
if self.show_advanced_options:
- self.data_schema[
- vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO)
- ] = str
+ self.data_schema[vol.Required(CONF_MYDEVOLO, default=self._url)] = str
if user_input is None:
return self._show_form(step_id="user")
try:
@@ -55,8 +55,36 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
step_id="zeroconf_confirm", errors={"base": "invalid_auth"}
)
+ async def async_step_reauth(self, user_input):
+ """Handle reauthentication."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
+ self._url = user_input[CONF_MYDEVOLO]
+ self.data_schema = {
+ vol.Required(CONF_USERNAME, default=user_input[CONF_USERNAME]): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+ return await self.async_step_reauth_confirm()
+
+ async def async_step_reauth_confirm(self, user_input=None):
+ """Handle a flow initiated by reauthentication."""
+ if user_input is None:
+ return self._show_form(step_id="reauth_confirm")
+ try:
+ return await self._connect_mydevolo(user_input)
+ except CredentialsInvalid:
+ return self._show_form(
+ step_id="reauth_confirm", errors={"base": "invalid_auth"}
+ )
+ except UuidChanged:
+ return self._show_form(
+ step_id="reauth_confirm", errors={"base": "reauth_failed"}
+ )
+
async def _connect_mydevolo(self, user_input):
"""Connect to mydevolo."""
+ user_input[CONF_MYDEVOLO] = user_input.get(CONF_MYDEVOLO, self._url)
mydevolo = configure_mydevolo(conf=user_input)
credentials_valid = await self.hass.async_add_executor_job(
mydevolo.credentials_valid
@@ -64,17 +92,30 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if not credentials_valid:
raise CredentialsInvalid
uuid = await self.hass.async_add_executor_job(mydevolo.uuid)
- await self.async_set_unique_id(uuid)
- self._abort_if_unique_id_configured()
- return self.async_create_entry(
- title="devolo Home Control",
- data={
- CONF_PASSWORD: mydevolo.password,
- CONF_USERNAME: mydevolo.user,
- CONF_MYDEVOLO: mydevolo.url,
- },
+ if not self._reauth_entry:
+ await self.async_set_unique_id(uuid)
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(
+ title="devolo Home Control",
+ data={
+ CONF_PASSWORD: mydevolo.password,
+ CONF_USERNAME: mydevolo.user,
+ CONF_MYDEVOLO: mydevolo.url,
+ },
+ )
+
+ if self._reauth_entry.unique_id != uuid:
+ # The old user and the new user are not the same. This could mess-up everything as all unique IDs might change.
+ raise UuidChanged
+
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry, data=user_input, unique_id=uuid
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
@callback
def _show_form(self, step_id, errors=None):
diff --git a/homeassistant/components/devolo_home_control/exceptions.py b/homeassistant/components/devolo_home_control/exceptions.py
index 378efa41cc5..a89058e6c16 100644
--- a/homeassistant/components/devolo_home_control/exceptions.py
+++ b/homeassistant/components/devolo_home_control/exceptions.py
@@ -4,3 +4,7 @@ from homeassistant.exceptions import HomeAssistantError
class CredentialsInvalid(HomeAssistantError):
"""Given credentials are invalid."""
+
+
+class UuidChanged(HomeAssistantError):
+ """UUID of the user changed."""
diff --git a/homeassistant/components/devolo_home_control/strings.json b/homeassistant/components/devolo_home_control/strings.json
index cbc911fcd18..ba1bc20bfd2 100644
--- a/homeassistant/components/devolo_home_control/strings.json
+++ b/homeassistant/components/devolo_home_control/strings.json
@@ -1,10 +1,12 @@
{
"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": {
- "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "reauth_failed": "Please use the same mydevolo user as before."
},
"step": {
"user": {
diff --git a/homeassistant/components/devolo_home_control/translations/de.json b/homeassistant/components/devolo_home_control/translations/de.json
index c34ecdb5c34..4208d80eaec 100644
--- a/homeassistant/components/devolo_home_control/translations/de.json
+++ b/homeassistant/components/devolo_home_control/translations/de.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Konto wurde bereits konfiguriert"
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
},
"error": {
- "invalid_auth": "Ung\u00fcltige Authentifizierung"
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "reauth_failed": "Bitte verwende denselben mydevolo-Benutzer wie zuvor."
},
"step": {
"user": {
diff --git a/homeassistant/components/devolo_home_control/translations/en.json b/homeassistant/components/devolo_home_control/translations/en.json
index d1b8645072f..e5ea6a49cd8 100644
--- a/homeassistant/components/devolo_home_control/translations/en.json
+++ b/homeassistant/components/devolo_home_control/translations/en.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Account is already configured"
+ "already_configured": "Account is already configured",
+ "reauth_successful": "Re-authentication was successful"
},
"error": {
- "invalid_auth": "Invalid authentication"
+ "invalid_auth": "Invalid authentication",
+ "reauth_failed": "Please use the same mydevolo user as before."
},
"step": {
"user": {
diff --git a/homeassistant/components/devolo_home_control/translations/et.json b/homeassistant/components/devolo_home_control/translations/et.json
index 75d332456e5..8997f4952df 100644
--- a/homeassistant/components/devolo_home_control/translations/et.json
+++ b/homeassistant/components/devolo_home_control/translations/et.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Konto on juba seadistatud"
+ "already_configured": "Konto on juba seadistatud",
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
},
"error": {
- "invalid_auth": "Tuvastamise viga"
+ "invalid_auth": "Tuvastamise viga",
+ "reauth_failed": "Palun kasuta sama mydevolo kasutajat nagu varem."
},
"step": {
"user": {
diff --git a/homeassistant/components/devolo_home_control/translations/he.json b/homeassistant/components/devolo_home_control/translations/he.json
index ac90b3264ea..903818bf429 100644
--- a/homeassistant/components/devolo_home_control/translations/he.json
+++ b/homeassistant/components/devolo_home_control/translations/he.json
@@ -1,10 +1,23 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
"step": {
"user": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
- "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ "username": "\u05d3\u05d5\u05d0\u05e8 \u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9/ \u05de\u05d6\u05d4\u05d4 devolo"
+ }
+ },
+ "zeroconf_confirm": {
+ "data": {
+ "mydevolo_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 mydevolo",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05d3\u05d5\u05d0\"\u05dc / \u05de\u05d6\u05d4\u05d4 devolo"
}
}
}
diff --git a/homeassistant/components/devolo_home_control/translations/nl.json b/homeassistant/components/devolo_home_control/translations/nl.json
index 0ca81ba7911..c85c685597d 100644
--- a/homeassistant/components/devolo_home_control/translations/nl.json
+++ b/homeassistant/components/devolo_home_control/translations/nl.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Account is al geconfigureerd"
+ "already_configured": "Account is al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
- "invalid_auth": "Ongeldige authenticatie"
+ "invalid_auth": "Ongeldige authenticatie",
+ "reauth_failed": "Gelieve dezelfde mydevolo-gebruiker te gebruiken als voorheen."
},
"step": {
"user": {
diff --git a/homeassistant/components/devolo_home_control/translations/no.json b/homeassistant/components/devolo_home_control/translations/no.json
index 984d279257e..1f1ee69ae47 100644
--- a/homeassistant/components/devolo_home_control/translations/no.json
+++ b/homeassistant/components/devolo_home_control/translations/no.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Kontoen er allerede konfigurert"
+ "already_configured": "Kontoen er allerede konfigurert",
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket"
},
"error": {
- "invalid_auth": "Ugyldig godkjenning"
+ "invalid_auth": "Ugyldig godkjenning",
+ "reauth_failed": "Bruk samme mydevolo-bruker som f\u00f8r."
},
"step": {
"user": {
diff --git a/homeassistant/components/devolo_home_control/translations/ru.json b/homeassistant/components/devolo_home_control/translations/ru.json
index 7334ba2ad38..ab0463bc811 100644
--- a/homeassistant/components/devolo_home_control/translations/ru.json
+++ b/homeassistant/components/devolo_home_control/translations/ru.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant."
+ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
},
"error": {
- "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "reauth_failed": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0442\u043e\u0433\u043e \u0436\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f mydevolo, \u0447\u0442\u043e \u0438 \u0440\u0430\u043d\u044c\u0448\u0435."
},
"step": {
"user": {
diff --git a/homeassistant/components/devolo_home_control/translations/zh-Hant.json b/homeassistant/components/devolo_home_control/translations/zh-Hant.json
index 48aa0a9be2e..3bdd499bdbd 100644
--- a/homeassistant/components/devolo_home_control/translations/zh-Hant.json
+++ b/homeassistant/components/devolo_home_control/translations/zh-Hant.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
},
"error": {
- "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "reauth_failed": "\u8acb\u4f7f\u7528\u8207\u5148\u524d\u76f8\u540c\u7684 mydevolo \u4f7f\u7528\u8005\u3002"
},
"step": {
"user": {
diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py
index 1c02a86ca42..68622a23350 100644
--- a/homeassistant/components/dexcom/__init__.py
+++ b/homeassistant/components/dexcom/__init__.py
@@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=180)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Dexcom from a config entry."""
try:
dexcom = await hass.async_add_executor_job(
diff --git a/homeassistant/components/dexcom/translations/he.json b/homeassistant/components/dexcom/translations/he.json
new file mode 100644
index 00000000000..454b7e1ae51
--- /dev/null
+++ b/homeassistant/components/dexcom/translations/he.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dialogflow/translations/he.json b/homeassistant/components/dialogflow/translations/he.json
new file mode 100644
index 00000000000..ebee9aee976
--- /dev/null
+++ b/homeassistant/components/dialogflow/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "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.",
+ "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py
index b79a55394d5..2fec28db14a 100644
--- a/homeassistant/components/directv/__init__.py
+++ b/homeassistant/components/directv/__init__.py
@@ -6,21 +6,13 @@ from datetime import timedelta
from directv import DIRECTV, DIRECTVError
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_NAME, CONF_HOST
+from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.entity import DeviceInfo, Entity
-from .const import (
- ATTR_IDENTIFIERS,
- ATTR_MANUFACTURER,
- ATTR_MODEL,
- ATTR_SOFTWARE_VERSION,
- ATTR_VIA_DEVICE,
- DOMAIN,
-)
+from .const import DOMAIN
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
@@ -52,32 +44,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
-
-
-class DIRECTVEntity(Entity):
- """Defines a base DirecTV entity."""
-
- def __init__(self, *, dtv: DIRECTV, name: str, address: str = "0") -> None:
- """Initialize the DirecTV entity."""
- self._address = address
- self._device_id = address if address != "0" else dtv.device.info.receiver_id
- self._is_client = address != "0"
- self._name = name
- self.dtv = dtv
-
- @property
- def name(self) -> str:
- """Return the name of the entity."""
- return self._name
-
- @property
- def device_info(self) -> DeviceInfo:
- """Return device information about this DirecTV receiver."""
- return {
- ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)},
- ATTR_NAME: self.name,
- ATTR_MANUFACTURER: self.dtv.device.info.brand,
- ATTR_MODEL: None,
- ATTR_SOFTWARE_VERSION: self.dtv.device.info.version,
- ATTR_VIA_DEVICE: (DOMAIN, self.dtv.device.info.receiver_id),
- }
diff --git a/homeassistant/components/directv/entity.py b/homeassistant/components/directv/entity.py
new file mode 100644
index 00000000000..c632ad7e84c
--- /dev/null
+++ b/homeassistant/components/directv/entity.py
@@ -0,0 +1,39 @@
+"""Base DirecTV Entity."""
+from __future__ import annotations
+
+from directv import DIRECTV
+
+from homeassistant.const import ATTR_NAME
+from homeassistant.helpers.entity import DeviceInfo, Entity
+
+from .const import (
+ ATTR_IDENTIFIERS,
+ ATTR_MANUFACTURER,
+ ATTR_MODEL,
+ ATTR_SOFTWARE_VERSION,
+ ATTR_VIA_DEVICE,
+ DOMAIN,
+)
+
+
+class DIRECTVEntity(Entity):
+ """Defines a base DirecTV entity."""
+
+ def __init__(self, *, dtv: DIRECTV, address: str = "0") -> None:
+ """Initialize the DirecTV entity."""
+ self._address = address
+ self._device_id = address if address != "0" else dtv.device.info.receiver_id
+ self._is_client = address != "0"
+ self.dtv = dtv
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device information about this DirecTV receiver."""
+ return {
+ ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)},
+ ATTR_NAME: self.name,
+ ATTR_MANUFACTURER: self.dtv.device.info.brand,
+ ATTR_MODEL: None,
+ ATTR_SOFTWARE_VERSION: self.dtv.device.info.version,
+ ATTR_VIA_DEVICE: (DOMAIN, self.dtv.device.info.receiver_id),
+ }
diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py
index 5d7f7d1185b..1a7d07c5ebd 100644
--- a/homeassistant/components/directv/media_player.py
+++ b/homeassistant/components/directv/media_player.py
@@ -29,7 +29,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
-from . import DIRECTVEntity
from .const import (
ATTR_MEDIA_CURRENTLY_RECORDING,
ATTR_MEDIA_RATING,
@@ -37,6 +36,7 @@ from .const import (
ATTR_MEDIA_START_TIME,
DOMAIN,
)
+from .entity import DIRECTVEntity
_LOGGER = logging.getLogger(__name__)
@@ -91,12 +91,15 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity):
"""Initialize DirecTV media player."""
super().__init__(
dtv=dtv,
- name=name,
address=address,
)
- self._assumed_state = None
- self._available = False
+ self._attr_unique_id = self._device_id
+ self._attr_name = name
+ self._attr_device_class = DEVICE_CLASS_RECEIVER
+ self._attr_available = False
+ self._attr_assumed_state = None
+
self._is_recorded = None
self._is_standby = True
self._last_position = None
@@ -108,12 +111,12 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity):
async def async_update(self):
"""Retrieve latest state."""
self._state = await self.dtv.state(self._address)
- self._available = self._state.available
+ self._attr_available = self._state.available
self._is_standby = self._state.standby
self._program = self._state.program
if self._is_standby:
- self._assumed_state = False
+ self._attr_assumed_state = False
self._is_recorded = None
self._last_position = None
self._last_update = None
@@ -123,7 +126,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity):
self._is_recorded = self._program.recorded
self._last_position = self._program.position
self._last_update = self._state.at
- self._assumed_state = self._is_recorded
+ self._attr_assumed_state = self._is_recorded
@property
def extra_state_attributes(self):
@@ -137,24 +140,6 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity):
ATTR_MEDIA_START_TIME: self.media_start_time,
}
- @property
- def name(self):
- """Return the name of the device."""
- return self._name
-
- @property
- def device_class(self) -> str | None:
- """Return the class of this device."""
- return DEVICE_CLASS_RECEIVER
-
- @property
- def unique_id(self):
- """Return a unique ID to use for this media player."""
- if self._address == "0":
- return self.dtv.device.info.receiver_id
-
- return self._address
-
# MediaPlayerEntity properties and methods
@property
def state(self):
@@ -170,16 +155,6 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity):
return STATE_PLAYING
- @property
- def available(self):
- """Return if able to retrieve information from DVR or not."""
- return self._available
-
- @property
- def assumed_state(self):
- """Return if we assume the state or not."""
- return self._assumed_state
-
@property
def media_content_id(self):
"""Return the content ID of current playing media."""
@@ -316,7 +291,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity):
if self._is_client:
raise NotImplementedError()
- _LOGGER.debug("Turn on %s", self._name)
+ _LOGGER.debug("Turn on %s", self.name)
await self.dtv.remote("poweron", self._address)
async def async_turn_off(self):
@@ -324,32 +299,32 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity):
if self._is_client:
raise NotImplementedError()
- _LOGGER.debug("Turn off %s", self._name)
+ _LOGGER.debug("Turn off %s", self.name)
await self.dtv.remote("poweroff", self._address)
async def async_media_play(self):
"""Send play command."""
- _LOGGER.debug("Play on %s", self._name)
+ _LOGGER.debug("Play on %s", self.name)
await self.dtv.remote("play", self._address)
async def async_media_pause(self):
"""Send pause command."""
- _LOGGER.debug("Pause on %s", self._name)
+ _LOGGER.debug("Pause on %s", self.name)
await self.dtv.remote("pause", self._address)
async def async_media_stop(self):
"""Send stop command."""
- _LOGGER.debug("Stop on %s", self._name)
+ _LOGGER.debug("Stop on %s", self.name)
await self.dtv.remote("stop", self._address)
async def async_media_previous_track(self):
"""Send rewind command."""
- _LOGGER.debug("Rewind on %s", self._name)
+ _LOGGER.debug("Rewind on %s", self.name)
await self.dtv.remote("rew", self._address)
async def async_media_next_track(self):
"""Send fast forward command."""
- _LOGGER.debug("Fast forward on %s", self._name)
+ _LOGGER.debug("Fast forward on %s", self.name)
await self.dtv.remote("ffwd", self._address)
async def async_play_media(self, media_type, media_id, **kwargs):
@@ -362,5 +337,5 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity):
)
return
- _LOGGER.debug("Changing channel on %s to %s", self._name, media_id)
+ _LOGGER.debug("Changing channel on %s to %s", self.name, media_id)
await self.dtv.tune(media_id, self._address)
diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py
index 424b5ba4ec6..52e94bc2608 100644
--- a/homeassistant/components/directv/remote.py
+++ b/homeassistant/components/directv/remote.py
@@ -13,8 +13,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import DIRECTVEntity
from .const import DOMAIN
+from .entity import DIRECTVEntity
_LOGGER = logging.getLogger(__name__)
@@ -49,41 +49,24 @@ class DIRECTVRemote(DIRECTVEntity, RemoteEntity):
"""Initialize DirecTV remote."""
super().__init__(
dtv=dtv,
- name=name,
address=address,
)
- self._available = False
- self._is_on = True
-
- @property
- def available(self):
- """Return if able to retrieve information from device or not."""
- return self._available
-
- @property
- def unique_id(self):
- """Return a unique ID."""
- if self._address == "0":
- return self.dtv.device.info.receiver_id
-
- return self._address
-
- @property
- def is_on(self) -> bool:
- """Return True if entity is on."""
- return self._is_on
+ self._attr_unique_id = self._device_id
+ self._attr_name = name
+ self._attr_available = False
+ self._attr_is_on = True
async def async_update(self) -> None:
"""Update device state."""
status = await self.dtv.status(self._address)
if status in ("active", "standby"):
- self._available = True
- self._is_on = status == "active"
+ self._attr_available = True
+ self._attr_is_on = status == "active"
else:
- self._available = False
- self._is_on = False
+ self._attr_available = False
+ self._attr_is_on = False
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
diff --git a/homeassistant/components/directv/strings.json b/homeassistant/components/directv/strings.json
index e6c54d0d4aa..4384867dfa4 100644
--- a/homeassistant/components/directv/strings.json
+++ b/homeassistant/components/directv/strings.json
@@ -3,7 +3,6 @@
"flow_title": "{name}",
"step": {
"ssdp_confirm": {
- "data": {},
"description": "Do you want to set up {name}?"
},
"user": {
diff --git a/homeassistant/components/directv/translations/de.json b/homeassistant/components/directv/translations/de.json
index 95bd807c048..de5cc512940 100644
--- a/homeassistant/components/directv/translations/de.json
+++ b/homeassistant/components/directv/translations/de.json
@@ -7,7 +7,7 @@
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
},
- "flow_title": "DirecTV: {name}",
+ "flow_title": "{name}",
"step": {
"ssdp_confirm": {
"data": {
diff --git a/homeassistant/components/directv/translations/he.json b/homeassistant/components/directv/translations/he.json
new file mode 100644
index 00000000000..f057c4e4629
--- /dev/null
+++ b/homeassistant/components/directv/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "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": "{name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/directv/translations/it.json b/homeassistant/components/directv/translations/it.json
index 2f8e5f29943..9fb0932c342 100644
--- a/homeassistant/components/directv/translations/it.json
+++ b/homeassistant/components/directv/translations/it.json
@@ -10,6 +10,10 @@
"flow_title": "{name}",
"step": {
"ssdp_confirm": {
+ "data": {
+ "one": "Pi\u00f9",
+ "other": "Altri"
+ },
"description": "Vuoi impostare {name} ?"
},
"user": {
diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py
index dfc89a4cb7e..6d3dc704d83 100644
--- a/homeassistant/components/discord/notify.py
+++ b/homeassistant/components/discord/notify.py
@@ -112,7 +112,6 @@ class DiscordNotificationService(BaseNotificationService):
await channel.send(message, files=files)
except (discord.errors.HTTPException, discord.errors.NotFound) as error:
_LOGGER.warning("Communication error: %s", error)
- await discord_bot.logout()
await discord_bot.close()
# Using reconnect=False prevents multiple ready events to be fired.
diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py
index 883958226d8..5b6bb7a5372 100644
--- a/homeassistant/components/discovery/__init__.py
+++ b/homeassistant/components/discovery/__init__.py
@@ -13,6 +13,7 @@ from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_discover, async_load_platform
from homeassistant.helpers.event import async_track_point_in_utc_time
+from homeassistant.loader import async_get_zeroconf
import homeassistant.util.dt as dt_util
DOMAIN = "discovery"
@@ -139,6 +140,10 @@ async def async_setup(hass, config):
)
zeroconf_instance = await zeroconf.async_get_instance(hass)
+ # Do not scan for types that have already been converted
+ # as it will generate excess network traffic for questions
+ # the zeroconf instance already knows the answers
+ zeroconf_types = list(await async_get_zeroconf(hass))
async def new_service_found(service, info):
"""Handle a new service if one is found."""
@@ -187,7 +192,7 @@ async def async_setup(hass, config):
"""Scan for devices."""
try:
results = await hass.async_add_executor_job(
- _discover, netdisco, zeroconf_instance
+ _discover, netdisco, zeroconf_instance, zeroconf_types
)
for result in results:
@@ -209,11 +214,13 @@ async def async_setup(hass, config):
return True
-def _discover(netdisco, zeroconf_instance):
+def _discover(netdisco, zeroconf_instance, zeroconf_types):
"""Discover devices."""
results = []
try:
- netdisco.scan(zeroconf_instance=zeroconf_instance)
+ netdisco.scan(
+ zeroconf_instance=zeroconf_instance, suppress_mdns_types=zeroconf_types
+ )
for disc in netdisco.discover():
for service in netdisco.get_info(disc):
diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json
index a2d2df1730a..558c727c62c 100644
--- a/homeassistant/components/discovery/manifest.json
+++ b/homeassistant/components/discovery/manifest.json
@@ -2,7 +2,7 @@
"domain": "discovery",
"name": "Discovery",
"documentation": "https://www.home-assistant.io/integrations/discovery",
- "requirements": ["netdisco==2.8.3"],
+ "requirements": ["netdisco==2.9.0"],
"after_dependencies": ["zeroconf"],
"codeowners": [],
"quality_scale": "internal"
diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json
index 434ff0e9c39..87730aa1316 100644
--- a/homeassistant/components/dlna_dmr/manifest.json
+++ b/homeassistant/components/dlna_dmr/manifest.json
@@ -2,7 +2,7 @@
"domain": "dlna_dmr",
"name": "DLNA Digital Media Renderer",
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
- "requirements": ["async-upnp-client==0.18.0"],
+ "requirements": ["async-upnp-client==0.19.0"],
"codeowners": [],
"iot_class": "local_push"
}
diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py
index 01d6e2f4f2a..2fb0e30da90 100644
--- a/homeassistant/components/dnsip/sensor.py
+++ b/homeassistant/components/dnsip/sensor.py
@@ -1,4 +1,6 @@
"""Get your own public IP address or that of any host."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
@@ -8,7 +10,10 @@ import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
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
_LOGGER = logging.getLogger(__name__)
@@ -36,57 +41,44 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
-async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+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."""
hostname = config[CONF_HOSTNAME]
name = config.get(CONF_NAME)
- if not name:
- if hostname == DEFAULT_HOSTNAME:
- name = DEFAULT_NAME
- else:
- name = hostname
ipv6 = config[CONF_IPV6]
- if ipv6:
- resolver = config[CONF_RESOLVER_IPV6]
- else:
- resolver = config[CONF_RESOLVER]
- async_add_devices([WanIpSensor(hass, name, hostname, resolver, ipv6)], True)
+ if not name:
+ name = DEFAULT_NAME if hostname == DEFAULT_HOSTNAME else hostname
+ resolver = config[CONF_RESOLVER_IPV6] if ipv6 else config[CONF_RESOLVER]
+
+ async_add_devices([WanIpSensor(name, hostname, resolver, ipv6)], True)
class WanIpSensor(SensorEntity):
"""Implementation of a DNS IP sensor."""
- def __init__(self, hass, name, hostname, resolver, ipv6):
+ def __init__(self, name: str, hostname: str, resolver: str, ipv6: bool) -> None:
"""Initialize the DNS IP sensor."""
-
- self.hass = hass
- self._name = name
+ self._attr_name = name
self.hostname = hostname
self.resolver = aiodns.DNSResolver()
self.resolver.nameservers = [resolver]
self.querytype = "AAAA" if ipv6 else "A"
- self._state = None
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def state(self):
- """Return the current DNS IP address for hostname."""
- return self._state
-
- async def async_update(self):
+ async def async_update(self) -> None:
"""Get the current DNS IP address for hostname."""
-
try:
response = await self.resolver.query(self.hostname, self.querytype)
except DNSError as err:
_LOGGER.warning("Exception while resolving host: %s", err)
response = None
+
if response:
- self._state = response[0].host
+ self._attr_state = response[0].host
else:
- self._state = None
+ self._attr_state = None
diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json
index 4e31ca03371..ae584af5916 100644
--- a/homeassistant/components/doods/manifest.json
+++ b/homeassistant/components/doods/manifest.json
@@ -2,7 +2,7 @@
"domain": "doods",
"name": "DOODS - Dedicated Open Object Detection Service",
"documentation": "https://www.home-assistant.io/integrations/doods",
- "requirements": ["pydoods==1.0.2", "pillow==8.1.2"],
+ "requirements": ["pydoods==1.0.2", "pillow==8.2.0"],
"codeowners": [],
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py
index 30c2613f0d5..f0579ef900b 100644
--- a/homeassistant/components/doorbird/__init__.py
+++ b/homeassistant/components/doorbird/__init__.py
@@ -90,7 +90,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up DoorBird from a config entry."""
_async_import_options_from_data_if_missing(hass, entry)
diff --git a/homeassistant/components/doorbird/translations/de.json b/homeassistant/components/doorbird/translations/de.json
index 0d6bef7a63f..640f13a73c6 100644
--- a/homeassistant/components/doorbird/translations/de.json
+++ b/homeassistant/components/doorbird/translations/de.json
@@ -10,7 +10,7 @@
"invalid_auth": "Ung\u00fcltige Authentifikation",
"unknown": "Unerwarteter Fehler"
},
- "flow_title": "DoorBird {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/doorbird/translations/he.json b/homeassistant/components/doorbird/translations/he.json
index f08cbbdff11..5143667adfb 100644
--- a/homeassistant/components/doorbird/translations/he.json
+++ b/homeassistant/components/doorbird/translations/he.json
@@ -1,11 +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",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05e6\u05e4\u05d5\u05d9\u05d9\u05d4"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py
index 3af620df19c..0e238363fc0 100644
--- a/homeassistant/components/dsmr/__init__.py
+++ b/homeassistant/components/dsmr/__init__.py
@@ -5,26 +5,23 @@ from contextlib import suppress
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
-from .const import DATA_LISTENER, DATA_TASK, DOMAIN, PLATFORMS
+from .const import DATA_TASK, DOMAIN, PLATFORMS
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up DSMR from a config entry."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {}
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
-
- listener = entry.add_update_listener(async_update_options)
- hass.data[DOMAIN][entry.entry_id][DATA_LISTENER] = listener
+ entry.async_on_unload(entry.add_update_listener(async_update_options))
return True
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
task = hass.data[DOMAIN][entry.entry_id][DATA_TASK]
- listener = hass.data[DOMAIN][entry.entry_id][DATA_LISTENER]
# Cancel the reconnect task
task.cancel()
@@ -33,13 +30,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
- listener()
-
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
-async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update options."""
- await hass.config_entries.async_reload(config_entry.entry_id)
+ await hass.config_entries.async_reload(entry.entry_id)
diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py
index b349afb28c7..72e854fe43a 100644
--- a/homeassistant/components/dsmr/config_flow.py
+++ b/homeassistant/components/dsmr/config_flow.py
@@ -3,18 +3,23 @@ from __future__ import annotations
import asyncio
from functools import partial
-import logging
+import os
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.objects import DSMRObject
import serial
+import serial.tools.list_ports
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
from homeassistant.core import callback
+from homeassistant.data_entry_flow import FlowResult
+from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_DSMR_VERSION,
@@ -23,41 +28,45 @@ from .const import (
CONF_TIME_BETWEEN_UPDATE,
DEFAULT_TIME_BETWEEN_UPDATE,
DOMAIN,
+ LOGGER,
)
-_LOGGER = logging.getLogger(__name__)
+CONF_MANUAL_PATH = "Enter Manually"
class DSMRConnection:
"""Test the connection to DSMR and receive telegram to read serial ids."""
- def __init__(self, host, port, dsmr_version):
+ def __init__(self, host: str | None, port: int, dsmr_version: str) -> None:
"""Initialize."""
self._host = host
self._port = port
self._dsmr_version = dsmr_version
- self._telegram = {}
+ self._telegram: dict[str, DSMRObject] = {}
+ self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER
if dsmr_version == "5L":
self._equipment_identifier = obis_ref.LUXEMBOURG_EQUIPMENT_IDENTIFIER
- else:
- self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER
- def equipment_identifier(self):
+ def equipment_identifier(self) -> str | None:
"""Equipment identifier."""
if self._equipment_identifier in self._telegram:
dsmr_object = self._telegram[self._equipment_identifier]
- return getattr(dsmr_object, "value", None)
+ identifier: str | None = getattr(dsmr_object, "value", None)
+ return identifier
+ return None
- def equipment_identifier_gas(self):
+ def equipment_identifier_gas(self) -> str | None:
"""Equipment identifier gas."""
if obis_ref.EQUIPMENT_IDENTIFIER_GAS in self._telegram:
dsmr_object = self._telegram[obis_ref.EQUIPMENT_IDENTIFIER_GAS]
- return getattr(dsmr_object, "value", None)
+ identifier: str | None = getattr(dsmr_object, "value", None)
+ return identifier
+ return None
async def validate_connect(self, hass: core.HomeAssistant) -> bool:
"""Test if we can validate connection with the device."""
- def update_telegram(telegram):
+ def update_telegram(telegram: dict[str, DSMRObject]) -> None:
if self._equipment_identifier in telegram:
self._telegram = telegram
transport.close()
@@ -83,7 +92,7 @@ class DSMRConnection:
try:
transport, protocol = await asyncio.create_task(reader_factory())
except (serial.serialutil.SerialException, OSError):
- _LOGGER.exception("Error connecting to DSMR")
+ LOGGER.exception("Error connecting to DSMR")
return False
if transport:
@@ -97,7 +106,9 @@ class DSMRConnection:
return True
-async def _validate_dsmr_connection(hass: core.HomeAssistant, data):
+async def _validate_dsmr_connection(
+ hass: core.HomeAssistant, data: dict[str, Any]
+) -> 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])
@@ -111,32 +122,32 @@ async def _validate_dsmr_connection(hass: core.HomeAssistant, data):
if equipment_identifier is None:
raise CannotCommunicate
- info = {
+ return {
CONF_SERIAL_ID: equipment_identifier,
CONF_SERIAL_ID_GAS: equipment_identifier_gas,
}
- return info
-
class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for DSMR."""
VERSION = 1
+ _dsmr_version: str | None = None
+
@staticmethod
@callback
- def async_get_options_flow(config_entry):
+ def async_get_options_flow(config_entry: ConfigEntry) -> DSMROptionFlowHandler:
"""Get the options flow for this handler."""
return DSMROptionFlowHandler(config_entry)
def _abort_if_host_port_configured(
self,
port: str,
- host: str = None,
+ host: str | None = None,
updates: dict[Any, Any] | None = None,
reload_on_update: bool = True,
- ):
+ ) -> FlowResult | None:
"""Test if host and port are already configured."""
for entry in self._async_current_entries():
if entry.data.get(CONF_HOST) == host and entry.data[CONF_PORT] == port:
@@ -160,7 +171,133 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return None
- async def async_step_import(self, import_config=None):
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Step when user initializes a integration."""
+ if user_input is not None:
+ user_selection = user_input[CONF_TYPE]
+ if user_selection == "Serial":
+ return await self.async_step_setup_serial()
+
+ return await self.async_step_setup_network()
+
+ list_of_types = ["Serial", "Network"]
+
+ schema = vol.Schema({vol.Required(CONF_TYPE): vol.In(list_of_types)})
+ return self.async_show_form(step_id="user", data_schema=schema)
+
+ async def async_step_setup_network(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Step when setting up network configuration."""
+ errors: dict[str, str] = {}
+ if user_input is not None:
+ data = await self.async_validate_dsmr(user_input, errors)
+ if not errors:
+ return self.async_create_entry(
+ title=f"{data[CONF_HOST]}:{data[CONF_PORT]}", data=data
+ )
+
+ schema = vol.Schema(
+ {
+ vol.Required(CONF_HOST): str,
+ vol.Required(CONF_PORT): int,
+ vol.Required(CONF_DSMR_VERSION): vol.In(["2.2", "4", "5", "5B", "5L"]),
+ }
+ )
+ return self.async_show_form(
+ step_id="setup_network",
+ data_schema=schema,
+ errors=errors,
+ )
+
+ async def async_step_setup_serial(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Step when setting up serial configuration."""
+ errors: dict[str, str] = {}
+ if user_input is not None:
+ user_selection = user_input[CONF_PORT]
+ if user_selection == CONF_MANUAL_PATH:
+ self._dsmr_version = user_input[CONF_DSMR_VERSION]
+ return await self.async_step_setup_serial_manual_path()
+
+ dev_path = await self.hass.async_add_executor_job(
+ get_serial_by_id, user_selection
+ )
+
+ validate_data = {
+ CONF_PORT: dev_path,
+ CONF_DSMR_VERSION: user_input[CONF_DSMR_VERSION],
+ }
+
+ data = await self.async_validate_dsmr(validate_data, errors)
+ if not errors:
+ return self.async_create_entry(title=data[CONF_PORT], data=data)
+
+ ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports)
+ list_of_ports = {
+ port.device: f"{port}, s/n: {port.serial_number or 'n/a'}"
+ + (f" - {port.manufacturer}" if port.manufacturer else "")
+ for port in ports
+ }
+ list_of_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH
+
+ schema = vol.Schema(
+ {
+ vol.Required(CONF_PORT): vol.In(list_of_ports),
+ vol.Required(CONF_DSMR_VERSION): vol.In(["2.2", "4", "5", "5B", "5L"]),
+ }
+ )
+ return self.async_show_form(
+ step_id="setup_serial",
+ data_schema=schema,
+ errors=errors,
+ )
+
+ async def async_step_setup_serial_manual_path(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Select path manually."""
+ if user_input is not None:
+ validate_data = {
+ CONF_PORT: user_input[CONF_PORT],
+ CONF_DSMR_VERSION: self._dsmr_version,
+ }
+
+ errors: dict[str, str] = {}
+ data = await self.async_validate_dsmr(validate_data, errors)
+ if not errors:
+ return self.async_create_entry(title=data[CONF_PORT], data=data)
+
+ schema = vol.Schema({vol.Required(CONF_PORT): str})
+ return self.async_show_form(
+ step_id="setup_serial_manual_path",
+ data_schema=schema,
+ )
+
+ async def async_validate_dsmr(
+ self, input_data: dict[str, Any], errors: dict[str, str]
+ ) -> dict[str, Any]:
+ """Validate dsmr connection and create data."""
+ data = input_data
+
+ try:
+ info = await _validate_dsmr_connection(self.hass, data)
+
+ data = {**data, **info}
+
+ await self.async_set_unique_id(info[CONF_SERIAL_ID])
+ self._abort_if_unique_id_configured()
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+ except CannotCommunicate:
+ errors["base"] = "cannot_communicate"
+
+ return data
+
+ async def async_step_import(self, import_config: ConfigType) -> FlowResult:
"""Handle the initial step."""
host = import_config.get(CONF_HOST)
port = import_config[CONF_PORT]
@@ -176,11 +313,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
except CannotCommunicate:
return self.async_abort(reason="cannot_communicate")
- if host is not None:
- name = f"{host}:{port}"
- else:
- name = port
-
+ name = f"{host}:{port}" if host is not None else port
data = {**import_config, **info}
await self.async_set_unique_id(info[CONF_SERIAL_ID])
@@ -192,11 +325,13 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class DSMROptionFlowHandler(config_entries.OptionsFlow):
"""Handle options."""
- def __init__(self, config_entry):
+ def __init__(self, entry: ConfigEntry) -> None:
"""Initialize options flow."""
- self.config_entry = config_entry
+ self.entry = entry
- async def async_step_init(self, user_input=None):
+ async def async_step_init(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
@@ -207,7 +342,7 @@ class DSMROptionFlowHandler(config_entries.OptionsFlow):
{
vol.Optional(
CONF_TIME_BETWEEN_UPDATE,
- default=self.config_entry.options.get(
+ default=self.entry.options.get(
CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE
),
): vol.All(vol.Coerce(int), vol.Range(min=0)),
@@ -216,6 +351,18 @@ class DSMROptionFlowHandler(config_entries.OptionsFlow):
)
+def get_serial_by_id(dev_path: str) -> str:
+ """Return a /dev/serial/by-id match for given device if available."""
+ by_id = "/dev/serial/by-id"
+ if not os.path.isdir(by_id):
+ return dev_path
+
+ for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()):
+ if os.path.realpath(path) == dev_path:
+ return path
+ return dev_path
+
+
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py
index da804857845..b26caa5c865 100644
--- a/homeassistant/components/dsmr/const.py
+++ b/homeassistant/components/dsmr/const.py
@@ -1,7 +1,25 @@
"""Constants for the DSMR integration."""
+from __future__ import annotations
+
+import logging
+
+from dsmr_parser import obis_references
+
+from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
+from homeassistant.const import (
+ DEVICE_CLASS_CURRENT,
+ DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_POWER,
+ DEVICE_CLASS_VOLTAGE,
+)
+from homeassistant.util import dt
+
+from .models import DSMRSensor
DOMAIN = "dsmr"
+LOGGER = logging.getLogger(__package__)
+
PLATFORMS = ["sensor"]
CONF_DSMR_VERSION = "dsmr_version"
@@ -18,13 +36,247 @@ DEFAULT_PRECISION = 3
DEFAULT_RECONNECT_INTERVAL = 30
DEFAULT_TIME_BETWEEN_UPDATE = 30
-DATA_LISTENER = "listener"
DATA_TASK = "task"
DEVICE_NAME_ENERGY = "Energy Meter"
DEVICE_NAME_GAS = "Gas Meter"
-ICON_GAS = "mdi:fire"
-ICON_POWER = "mdi:flash"
-ICON_POWER_FAILURE = "mdi:flash-off"
-ICON_SWELL_SAG = "mdi:pulse"
+SENSORS: list[DSMRSensor] = [
+ DSMRSensor(
+ name="Power Consumption",
+ obis_reference=obis_references.CURRENT_ELECTRICITY_USAGE,
+ device_class=DEVICE_CLASS_POWER,
+ force_update=True,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Power Production",
+ obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY,
+ device_class=DEVICE_CLASS_POWER,
+ force_update=True,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Power Tariff",
+ obis_reference=obis_references.ELECTRICITY_ACTIVE_TARIFF,
+ icon="mdi:flash",
+ ),
+ DSMRSensor(
+ name="Energy Consumption (tarif 1)",
+ obis_reference=obis_references.ELECTRICITY_USED_TARIFF_1,
+ device_class=DEVICE_CLASS_ENERGY,
+ force_update=True,
+ last_reset=dt.utc_from_timestamp(0),
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Energy Consumption (tarif 2)",
+ obis_reference=obis_references.ELECTRICITY_USED_TARIFF_2,
+ force_update=True,
+ device_class=DEVICE_CLASS_ENERGY,
+ last_reset=dt.utc_from_timestamp(0),
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Energy Production (tarif 1)",
+ obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_1,
+ force_update=True,
+ device_class=DEVICE_CLASS_ENERGY,
+ last_reset=dt.utc_from_timestamp(0),
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Energy Production (tarif 2)",
+ obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_2,
+ force_update=True,
+ device_class=DEVICE_CLASS_ENERGY,
+ last_reset=dt.utc_from_timestamp(0),
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Power Consumption Phase L1",
+ obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE,
+ device_class=DEVICE_CLASS_POWER,
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Power Consumption Phase L2",
+ obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE,
+ device_class=DEVICE_CLASS_POWER,
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Power Consumption Phase L3",
+ obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE,
+ device_class=DEVICE_CLASS_POWER,
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Power Production Phase L1",
+ obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE,
+ device_class=DEVICE_CLASS_POWER,
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Power Production Phase L2",
+ obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE,
+ device_class=DEVICE_CLASS_POWER,
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Power Production Phase L3",
+ obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE,
+ device_class=DEVICE_CLASS_POWER,
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Short Power Failure Count",
+ obis_reference=obis_references.SHORT_POWER_FAILURE_COUNT,
+ entity_registry_enabled_default=False,
+ icon="mdi:flash-off",
+ ),
+ DSMRSensor(
+ name="Long Power Failure Count",
+ obis_reference=obis_references.LONG_POWER_FAILURE_COUNT,
+ entity_registry_enabled_default=False,
+ icon="mdi:flash-off",
+ ),
+ DSMRSensor(
+ name="Voltage Sags Phase L1",
+ obis_reference=obis_references.VOLTAGE_SAG_L1_COUNT,
+ entity_registry_enabled_default=False,
+ ),
+ DSMRSensor(
+ name="Voltage Sags Phase L2",
+ obis_reference=obis_references.VOLTAGE_SAG_L2_COUNT,
+ entity_registry_enabled_default=False,
+ ),
+ DSMRSensor(
+ name="Voltage Sags Phase L3",
+ obis_reference=obis_references.VOLTAGE_SAG_L3_COUNT,
+ entity_registry_enabled_default=False,
+ ),
+ DSMRSensor(
+ name="Voltage Swells Phase L1",
+ obis_reference=obis_references.VOLTAGE_SWELL_L1_COUNT,
+ entity_registry_enabled_default=False,
+ icon="mdi:pulse",
+ ),
+ DSMRSensor(
+ name="Voltage Swells Phase L2",
+ obis_reference=obis_references.VOLTAGE_SWELL_L2_COUNT,
+ entity_registry_enabled_default=False,
+ icon="mdi:pulse",
+ ),
+ DSMRSensor(
+ name="Voltage Swells Phase L3",
+ obis_reference=obis_references.VOLTAGE_SWELL_L3_COUNT,
+ entity_registry_enabled_default=False,
+ icon="mdi:pulse",
+ ),
+ DSMRSensor(
+ name="Voltage Phase L1",
+ obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L1,
+ device_class=DEVICE_CLASS_VOLTAGE,
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Voltage Phase L2",
+ obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L2,
+ device_class=DEVICE_CLASS_VOLTAGE,
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Voltage Phase L3",
+ obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L3,
+ device_class=DEVICE_CLASS_VOLTAGE,
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Current Phase L1",
+ obis_reference=obis_references.INSTANTANEOUS_CURRENT_L1,
+ device_class=DEVICE_CLASS_CURRENT,
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Current Phase L2",
+ obis_reference=obis_references.INSTANTANEOUS_CURRENT_L2,
+ device_class=DEVICE_CLASS_CURRENT,
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Current Phase L3",
+ obis_reference=obis_references.INSTANTANEOUS_CURRENT_L3,
+ device_class=DEVICE_CLASS_CURRENT,
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Energy Consumption (total)",
+ obis_reference=obis_references.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL,
+ dsmr_versions={"5L"},
+ force_update=True,
+ device_class=DEVICE_CLASS_ENERGY,
+ last_reset=dt.utc_from_timestamp(0),
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Energy Production (total)",
+ obis_reference=obis_references.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL,
+ dsmr_versions={"5L"},
+ force_update=True,
+ device_class=DEVICE_CLASS_ENERGY,
+ last_reset=dt.utc_from_timestamp(0),
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Energy Consumption (total)",
+ obis_reference=obis_references.ELECTRICITY_IMPORTED_TOTAL,
+ dsmr_versions={"2.2", "4", "5", "5B"},
+ force_update=True,
+ device_class=DEVICE_CLASS_ENERGY,
+ last_reset=dt.utc_from_timestamp(0),
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Gas Consumption",
+ obis_reference=obis_references.HOURLY_GAS_METER_READING,
+ dsmr_versions={"4", "5", "5L"},
+ is_gas=True,
+ force_update=True,
+ icon="mdi:fire",
+ last_reset=dt.utc_from_timestamp(0),
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Gas Consumption",
+ obis_reference=obis_references.BELGIUM_HOURLY_GAS_METER_READING,
+ dsmr_versions={"5B"},
+ is_gas=True,
+ force_update=True,
+ icon="mdi:fire",
+ last_reset=dt.utc_from_timestamp(0),
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ DSMRSensor(
+ name="Gas Consumption",
+ obis_reference=obis_references.GAS_METER_READING,
+ dsmr_versions={"2.2"},
+ is_gas=True,
+ force_update=True,
+ icon="mdi:fire",
+ last_reset=dt.utc_from_timestamp(0),
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+]
diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json
index a5c9b8e62bc..df738724ac0 100644
--- a/homeassistant/components/dsmr/manifest.json
+++ b/homeassistant/components/dsmr/manifest.json
@@ -3,7 +3,7 @@
"name": "DSMR Slimme Meter",
"documentation": "https://www.home-assistant.io/integrations/dsmr",
"requirements": ["dsmr_parser==0.29"],
- "codeowners": ["@Robbie1221"],
- "config_flow": false,
+ "codeowners": ["@Robbie1221", "@frenck"],
+ "config_flow": true,
"iot_class": "local_push"
}
diff --git a/homeassistant/components/dsmr/models.py b/homeassistant/components/dsmr/models.py
new file mode 100644
index 00000000000..b54a5af80d5
--- /dev/null
+++ b/homeassistant/components/dsmr/models.py
@@ -0,0 +1,22 @@
+"""Models for the DSMR integration."""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from datetime import datetime
+
+
+@dataclass
+class DSMRSensor:
+ """Represents an DSMR Sensor."""
+
+ name: str
+ obis_reference: str
+
+ device_class: str | None = None
+ dsmr_versions: set[str] | None = None
+ entity_registry_enabled_default: bool = True
+ force_update: bool = False
+ icon: str | None = None
+ is_gas: bool = False
+ last_reset: datetime | None = None
+ state_class: str | None = None
diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py
index 237f3b2f929..cfdcbd95cf4 100644
--- a/homeassistant/components/dsmr/sensor.py
+++ b/homeassistant/components/dsmr/sensor.py
@@ -6,24 +6,21 @@ from asyncio import CancelledError
from contextlib import suppress
from datetime import timedelta
from functools import partial
-import logging
+from typing import Any
from dsmr_parser import obis_references as obis_ref
from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
+from dsmr_parser.objects import DSMRObject
import serial
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
-from homeassistant.const import (
- CONF_HOST,
- CONF_PORT,
- EVENT_HOMEASSISTANT_STOP,
- TIME_HOURS,
-)
+from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CoreState, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, StateType
from homeassistant.util import Throttle
from .const import (
@@ -42,13 +39,10 @@ from .const import (
DEVICE_NAME_ENERGY,
DEVICE_NAME_GAS,
DOMAIN,
- ICON_GAS,
- ICON_POWER,
- ICON_POWER_FAILURE,
- ICON_SWELL_SAG,
+ LOGGER,
+ SENSORS,
)
-
-_LOGGER = logging.getLogger(__name__)
+from .models import DSMRSensor
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -63,8 +57,19 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: AddEntitiesCallback,
+ discovery_info: dict[str, Any] | None = None,
+) -> None:
"""Import the platform into a config entry."""
+ LOGGER.warning(
+ "Configuration of the DSMR platform in YAML is deprecated and will be "
+ "removed in Home Assistant 2021.9; Your existing configuration "
+ "has been imported into the UI automatically and can be safely removed "
+ "from your configuration.yaml file"
+ )
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
@@ -73,147 +78,37 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(
- hass: HomeAssistant, entry: ConfigEntry, async_add_entities
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the DSMR sensor."""
- config = entry.data
- options = entry.options
-
- dsmr_version = config[CONF_DSMR_VERSION]
-
- # Define list of name,obis,force_update mappings to generate entities
- obis_mapping = [
- ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE, True],
- ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY, True],
- ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF, False],
- ["Energy Consumption (tarif 1)", obis_ref.ELECTRICITY_USED_TARIFF_1, True],
- ["Energy Consumption (tarif 2)", obis_ref.ELECTRICITY_USED_TARIFF_2, True],
- ["Energy Production (tarif 1)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1, True],
- ["Energy Production (tarif 2)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2, True],
- [
- "Power Consumption Phase L1",
- obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE,
- False,
- ],
- [
- "Power Consumption Phase L2",
- obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE,
- False,
- ],
- [
- "Power Consumption Phase L3",
- obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE,
- False,
- ],
- [
- "Power Production Phase L1",
- obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE,
- False,
- ],
- [
- "Power Production Phase L2",
- obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE,
- False,
- ],
- [
- "Power Production Phase L3",
- obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE,
- False,
- ],
- ["Short Power Failure Count", obis_ref.SHORT_POWER_FAILURE_COUNT, False],
- ["Long Power Failure Count", obis_ref.LONG_POWER_FAILURE_COUNT, False],
- ["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT, False],
- ["Voltage Sags Phase L2", obis_ref.VOLTAGE_SAG_L2_COUNT, False],
- ["Voltage Sags Phase L3", obis_ref.VOLTAGE_SAG_L3_COUNT, False],
- ["Voltage Swells Phase L1", obis_ref.VOLTAGE_SWELL_L1_COUNT, False],
- ["Voltage Swells Phase L2", obis_ref.VOLTAGE_SWELL_L2_COUNT, False],
- ["Voltage Swells Phase L3", obis_ref.VOLTAGE_SWELL_L3_COUNT, False],
- ["Voltage Phase L1", obis_ref.INSTANTANEOUS_VOLTAGE_L1, False],
- ["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2, False],
- ["Voltage Phase L3", obis_ref.INSTANTANEOUS_VOLTAGE_L3, False],
- ["Current Phase L1", obis_ref.INSTANTANEOUS_CURRENT_L1, False],
- ["Current Phase L2", obis_ref.INSTANTANEOUS_CURRENT_L2, False],
- ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3, False],
+ dsmr_version = entry.data[CONF_DSMR_VERSION]
+ entities = [
+ DSMREntity(sensor, entry)
+ for sensor in SENSORS
+ if (sensor.dsmr_versions is None or dsmr_version in sensor.dsmr_versions)
+ and (not sensor.is_gas or CONF_SERIAL_ID_GAS in entry.data)
]
-
- if dsmr_version == "5L":
- obis_mapping.extend(
- [
- [
- "Energy Consumption (total)",
- obis_ref.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL,
- True,
- ],
- [
- "Energy Production (total)",
- obis_ref.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL,
- True,
- ],
- ]
- )
- else:
- obis_mapping.extend(
- [["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL, True]]
- )
-
- # Generate device entities
- devices = [
- DSMREntity(
- name, DEVICE_NAME_ENERGY, config[CONF_SERIAL_ID], obis, config, force_update
- )
- for name, obis, force_update in obis_mapping
- ]
-
- # Protocol version specific obis
- if CONF_SERIAL_ID_GAS in config:
- if dsmr_version in ("4", "5", "5L"):
- gas_obis = obis_ref.HOURLY_GAS_METER_READING
- elif dsmr_version in ("5B",):
- gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING
- else:
- gas_obis = obis_ref.GAS_METER_READING
-
- # Add gas meter reading and derivative for usage
- devices += [
- DSMREntity(
- "Gas Consumption",
- DEVICE_NAME_GAS,
- config[CONF_SERIAL_ID_GAS],
- gas_obis,
- config,
- True,
- ),
- DerivativeDSMREntity(
- "Hourly Gas Consumption",
- DEVICE_NAME_GAS,
- config[CONF_SERIAL_ID_GAS],
- gas_obis,
- config,
- False,
- ),
- ]
-
- async_add_entities(devices)
+ async_add_entities(entities)
min_time_between_updates = timedelta(
- seconds=options.get(CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE)
+ seconds=entry.options.get(CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE)
)
@Throttle(min_time_between_updates)
- def update_entities_telegram(telegram):
+ def update_entities_telegram(telegram: dict[str, DSMRObject]) -> None:
"""Update entities with latest telegram and trigger state update."""
# Make all device entities aware of new telegram
- for device in devices:
- device.update_data(telegram)
+ for entity in entities:
+ entity.update_data(telegram)
# Creates an asyncio.Protocol factory for reading DSMR telegrams from
# serial and calls update_entities_telegram to update entities on arrival
- if CONF_HOST in config:
+ if CONF_HOST in entry.data:
reader_factory = partial(
create_tcp_dsmr_reader,
- config[CONF_HOST],
- config[CONF_PORT],
- config[CONF_DSMR_VERSION],
+ entry.data[CONF_HOST],
+ entry.data[CONF_PORT],
+ entry.data[CONF_DSMR_VERSION],
update_entities_telegram,
loop=hass.loop,
keep_alive_interval=60,
@@ -221,13 +116,13 @@ async def async_setup_entry(
else:
reader_factory = partial(
create_dsmr_reader,
- config[CONF_PORT],
- config[CONF_DSMR_VERSION],
+ entry.data[CONF_PORT],
+ entry.data[CONF_DSMR_VERSION],
update_entities_telegram,
loop=hass.loop,
)
- async def connect_and_reconnect():
+ async def connect_and_reconnect() -> None:
"""Connect to DSMR and keep reconnecting until Home Assistant stops."""
stop_listener = None
transport = None
@@ -259,12 +154,12 @@ async def async_setup_entry(
update_entities_telegram({})
# throttle reconnect attempts
- await asyncio.sleep(config[CONF_RECONNECT_INTERVAL])
+ await asyncio.sleep(entry.data[CONF_RECONNECT_INTERVAL])
except (serial.serialutil.SerialException, OSError):
# Log any error while establishing connection and drop to retry
# connection wait
- _LOGGER.exception("Error connecting to DSMR")
+ LOGGER.exception("Error connecting to DSMR")
transport = None
protocol = None
except CancelledError:
@@ -289,62 +184,67 @@ async def async_setup_entry(
class DSMREntity(SensorEntity):
"""Entity reading values from DSMR telegram."""
- def __init__(self, name, device_name, device_serial, obis, config, force_update):
- """Initialize entity."""
- self._name = name
- self._obis = obis
- self._config = config
- self.telegram = {}
+ _attr_should_poll = False
- self._device_name = device_name
- self._device_serial = device_serial
- self._force_update = force_update
- self._unique_id = f"{device_serial}_{name}".replace(" ", "_")
+ def __init__(self, sensor: DSMRSensor, entry: ConfigEntry) -> None:
+ """Initialize entity."""
+ self._sensor = sensor
+ self._entry = entry
+ self.telegram: dict[str, DSMRObject] = {}
+
+ device_serial = entry.data[CONF_SERIAL_ID]
+ device_name = DEVICE_NAME_ENERGY
+ if sensor.is_gas:
+ device_serial = entry.data[CONF_SERIAL_ID_GAS]
+ device_name = DEVICE_NAME_GAS
+
+ self._attr_device_class = sensor.device_class
+ self._attr_device_info = {
+ "identifiers": {(DOMAIN, device_serial)},
+ "name": device_name,
+ }
+ self._attr_entity_registry_enabled_default = (
+ sensor.entity_registry_enabled_default
+ )
+ self._attr_force_update = sensor.force_update
+ self._attr_icon = sensor.icon
+ self._attr_last_reset = sensor.last_reset
+ self._attr_name = sensor.name
+ self._attr_state_class = sensor.state_class
+ self._attr_unique_id = f"{device_serial}_{sensor.name}".replace(" ", "_")
@callback
- def update_data(self, telegram):
+ def update_data(self, telegram: dict[str, DSMRObject]) -> None:
"""Update data."""
self.telegram = telegram
- if self.hass and self._obis in self.telegram:
+ if self.hass and self._sensor.obis_reference in self.telegram:
self.async_write_ha_state()
- def get_dsmr_object_attr(self, attribute):
+ def get_dsmr_object_attr(self, attribute: str) -> str | None:
"""Read attribute from last received telegram for this DSMR object."""
# Make sure telegram contains an object for this entities obis
- if self._obis not in self.telegram:
+ if self._sensor.obis_reference not in self.telegram:
return None
# Get the attribute value if the object has it
- dsmr_object = self.telegram[self._obis]
- return getattr(dsmr_object, attribute, None)
+ dsmr_object = self.telegram[self._sensor.obis_reference]
+ attr: str | None = getattr(dsmr_object, attribute)
+ return attr
@property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def icon(self):
- """Icon to use in the frontend, if any."""
- if "Sags" in self._name or "Swells" in self.name:
- return ICON_SWELL_SAG
- if "Failure" in self._name:
- return ICON_POWER_FAILURE
- if "Power" in self._name:
- return ICON_POWER
- if "Gas" in self._name:
- return ICON_GAS
-
- @property
- def state(self):
+ def state(self) -> StateType:
"""Return the state of sensor, if available, translate if needed."""
value = self.get_dsmr_object_attr("value")
+ if value is None:
+ return None
- if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF:
- return self.translate_tariff(value, self._config[CONF_DSMR_VERSION])
+ if self._sensor.obis_reference == obis_ref.ELECTRICITY_ACTIVE_TARIFF:
+ return self.translate_tariff(value, self._entry.data[CONF_DSMR_VERSION])
with suppress(TypeError):
- value = round(float(value), self._config[CONF_PRECISION])
+ value = round(
+ float(value), self._entry.data.get(CONF_PRECISION, DEFAULT_PRECISION)
+ )
if value is not None:
return value
@@ -352,39 +252,16 @@ class DSMREntity(SensorEntity):
return None
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of this entity, if any."""
return self.get_dsmr_object_attr("unit")
- @property
- def unique_id(self) -> str:
- """Return a unique ID."""
- return self._unique_id
-
- @property
- def device_info(self) -> DeviceInfo:
- """Return the device information."""
- return {
- "identifiers": {(DOMAIN, self._device_serial)},
- "name": self._device_name,
- }
-
- @property
- def force_update(self):
- """Force update."""
- return self._force_update
-
- @property
- def should_poll(self):
- """Disable polling."""
- return False
-
@staticmethod
- def translate_tariff(value, dsmr_version):
+ def translate_tariff(value: str, dsmr_version: str) -> str | None:
"""Convert 2/1 to normal/low depending on DSMR version."""
# DSMR V5B: Note: In Belgium values are swapped:
# Rate code 2 is used for low rate and rate code 1 is used for normal rate.
- if dsmr_version in ("5B",):
+ if dsmr_version == "5B":
if value == "0001":
value = "0002"
elif value == "0002":
@@ -397,66 +274,3 @@ class DSMREntity(SensorEntity):
return "low"
return None
-
-
-class DerivativeDSMREntity(DSMREntity):
- """Calculated derivative for values where the DSMR doesn't offer one.
-
- Gas readings are only reported per hour and don't offer a rate only
- the current meter reading. This entity converts subsequents readings
- into a hourly rate.
- """
-
- _previous_reading = None
- _previous_timestamp = None
- _state = None
-
- @property
- def state(self):
- """Return the calculated current hourly rate."""
- return self._state
-
- @property
- def force_update(self):
- """Disable force update."""
- return False
-
- @property
- def should_poll(self):
- """Enable polling."""
- return True
-
- async def async_update(self):
- """Recalculate hourly rate if timestamp has changed.
-
- DSMR updates gas meter reading every hour. Along with the new
- value a timestamp is provided for the reading. Test if the last
- known timestamp differs from the current one then calculate a
- new rate for the previous hour.
-
- """
- # check if the timestamp for the object differs from the previous one
- timestamp = self.get_dsmr_object_attr("datetime")
- if timestamp and timestamp != self._previous_timestamp:
- current_reading = self.get_dsmr_object_attr("value")
-
- if self._previous_reading is None:
- # Can't calculate rate without previous datapoint
- # just store current point
- pass
- else:
- # Recalculate the rate
- diff = current_reading - self._previous_reading
- timediff = timestamp - self._previous_timestamp
- total_seconds = timediff.total_seconds()
- self._state = round(float(diff) / total_seconds * 3600, 3)
-
- self._previous_reading = current_reading
- self._previous_timestamp = timestamp
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, per hour, if any."""
- unit = self.get_dsmr_object_attr("unit")
- if unit:
- return f"{unit}/{TIME_HOURS}"
diff --git a/homeassistant/components/dsmr/strings.json b/homeassistant/components/dsmr/strings.json
index 57d38f78feb..cc9cd2ae86a 100644
--- a/homeassistant/components/dsmr/strings.json
+++ b/homeassistant/components/dsmr/strings.json
@@ -1,9 +1,43 @@
{
"config": {
- "step": {},
- "error": {},
+ "step": {
+ "user": {
+ "data": {
+ "type": "Connection type"
+ },
+ "title": "Select connection type"
+ },
+ "setup_network": {
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]",
+ "port": "[%key:common::config_flow::data::port%]",
+ "dsmr_version": "Select DSMR version"
+ },
+ "title": "Select connection address"
+ },
+ "setup_serial": {
+ "data": {
+ "port": "Select device",
+ "dsmr_version": "Select DSMR version"
+ },
+ "title": "Device"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "port": "[%key:common::config_flow::data::usb_path%]"
+ },
+ "title": "Path"
+ }
+ },
+ "error": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "cannot_communicate": "Failed to communicate"
+ },
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "cannot_communicate": "Failed to communicate"
}
},
"options": {
diff --git a/homeassistant/components/dsmr/translations/ca.json b/homeassistant/components/dsmr/translations/ca.json
index a876776fea2..263cb388980 100644
--- a/homeassistant/components/dsmr/translations/ca.json
+++ b/homeassistant/components/dsmr/translations/ca.json
@@ -1,7 +1,43 @@
{
"config": {
"abort": {
- "already_configured": "El dispositiu ja est\u00e0 configurat"
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_communicate": "No s'ha pogut comunicar",
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "error": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_communicate": "No s'ha pogut comunicar",
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "dsmr_version": "Selecciona la versi\u00f3 DSMR",
+ "host": "Amfitri\u00f3",
+ "port": "Port"
+ },
+ "title": "Selecciona l'adre\u00e7a de connexi\u00f3"
+ },
+ "setup_serial": {
+ "data": {
+ "dsmr_version": "Selecciona la versi\u00f3 DSMR",
+ "port": "Selecciona el dispositiu"
+ },
+ "title": "Dispositiu"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "port": "Ruta del port USB del dispositiu"
+ },
+ "title": "Ruta"
+ },
+ "user": {
+ "data": {
+ "type": "Tipus de connexi\u00f3"
+ },
+ "title": "Selecciona el tipus de connexi\u00f3"
+ }
}
},
"options": {
diff --git a/homeassistant/components/dsmr/translations/de.json b/homeassistant/components/dsmr/translations/de.json
index 97d6739b787..fe94109634a 100644
--- a/homeassistant/components/dsmr/translations/de.json
+++ b/homeassistant/components/dsmr/translations/de.json
@@ -1,7 +1,43 @@
{
"config": {
"abort": {
- "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "cannot_communicate": "Kommunikation fehlgeschlagen",
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "error": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "cannot_communicate": "Kommunikation fehlgeschlagen",
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "dsmr_version": "DSMR-Version ausw\u00e4hlen",
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Verbindungsadresse ausw\u00e4hlen"
+ },
+ "setup_serial": {
+ "data": {
+ "dsmr_version": "DSMR-Version ausw\u00e4hlen",
+ "port": "Ger\u00e4t w\u00e4hlen"
+ },
+ "title": "Ger\u00e4t"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "port": "USB-Ger\u00e4te-Pfad"
+ },
+ "title": "Pfad"
+ },
+ "user": {
+ "data": {
+ "type": "Verbindungstyp"
+ },
+ "title": "Verbindungstyp ausw\u00e4hlen"
+ }
}
},
"options": {
diff --git a/homeassistant/components/dsmr/translations/en.json b/homeassistant/components/dsmr/translations/en.json
index 159ede41b4e..6f873729bc8 100644
--- a/homeassistant/components/dsmr/translations/en.json
+++ b/homeassistant/components/dsmr/translations/en.json
@@ -1,7 +1,43 @@
{
"config": {
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "Device is already configured",
+ "cannot_communicate": "Failed to communicate",
+ "cannot_connect": "Failed to connect"
+ },
+ "error": {
+ "already_configured": "Device is already configured",
+ "cannot_communicate": "Failed to communicate",
+ "cannot_connect": "Failed to connect"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "dsmr_version": "Select DSMR version",
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Select connection address"
+ },
+ "setup_serial": {
+ "data": {
+ "dsmr_version": "Select DSMR version",
+ "port": "Select device"
+ },
+ "title": "Device"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "port": "USB Device Path"
+ },
+ "title": "Path"
+ },
+ "user": {
+ "data": {
+ "type": "Connection type"
+ },
+ "title": "Select connection type"
+ }
}
},
"options": {
diff --git a/homeassistant/components/dsmr/translations/et.json b/homeassistant/components/dsmr/translations/et.json
index 67f37f26586..5e9131621d3 100644
--- a/homeassistant/components/dsmr/translations/et.json
+++ b/homeassistant/components/dsmr/translations/et.json
@@ -1,15 +1,47 @@
{
"config": {
"abort": {
- "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "cannot_communicate": "\u00dchendamine nurjus",
+ "cannot_connect": "\u00dchendamine nurjus"
},
"error": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "cannot_communicate": "\u00dchendamine nurjus",
+ "cannot_connect": "\u00dchendamine nurjus",
"one": "\u00fcks",
"other": "mitu"
},
"step": {
"one": "\u00fcks",
- "other": "mitu"
+ "other": "mitu",
+ "setup_network": {
+ "data": {
+ "dsmr_version": "Vali DSMR versioon",
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Vali \u00fchenduse aadress"
+ },
+ "setup_serial": {
+ "data": {
+ "dsmr_version": "Vali DSMR versioon",
+ "port": "Vali seade"
+ },
+ "title": "Seade"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "port": "USB-seadme asukoha rada"
+ },
+ "title": "Rada"
+ },
+ "user": {
+ "data": {
+ "type": "\u00dchenduse t\u00fc\u00fcp"
+ },
+ "title": "Vali \u00fchenduse t\u00fc\u00fcp"
+ }
}
},
"options": {
diff --git a/homeassistant/components/dsmr/translations/he.json b/homeassistant/components/dsmr/translations/he.json
new file mode 100644
index 00000000000..cdb921611c4
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/he.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/hu.json b/homeassistant/components/dsmr/translations/hu.json
index 930b739fb18..76ad4dc653f 100644
--- a/homeassistant/components/dsmr/translations/hu.json
+++ b/homeassistant/components/dsmr/translations/hu.json
@@ -1,7 +1,26 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_communicate": "Nem siker\u00fclt csatlakozni."
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "dsmr_version": "DSMR verzi\u00f3 kiv\u00e1laszt\u00e1sa"
+ }
+ },
+ "setup_serial": {
+ "data": {
+ "port": "Eszk\u00f6z kiv\u00e1laszt\u00e1sa"
+ },
+ "title": "Eszk\u00f6z"
+ },
+ "user": {
+ "data": {
+ "type": "Kapcsolat t\u00edpusa"
+ }
+ }
}
},
"options": {
diff --git a/homeassistant/components/dsmr/translations/it.json b/homeassistant/components/dsmr/translations/it.json
index 75cbb713056..e1287c4af39 100644
--- a/homeassistant/components/dsmr/translations/it.json
+++ b/homeassistant/components/dsmr/translations/it.json
@@ -1,7 +1,47 @@
{
"config": {
"abort": {
- "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_communicate": "Impossibile comunicare",
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "error": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_communicate": "Impossibile comunicare",
+ "cannot_connect": "Impossibile connettersi",
+ "one": "Pi\u00f9",
+ "other": "Altri"
+ },
+ "step": {
+ "one": "Pi\u00f9",
+ "other": "Altri",
+ "setup_network": {
+ "data": {
+ "dsmr_version": "Seleziona la versione DSMR",
+ "host": "Host",
+ "port": "Porta"
+ },
+ "title": "Seleziona l'indirizzo di connessione"
+ },
+ "setup_serial": {
+ "data": {
+ "dsmr_version": "Seleziona la versione DSMR",
+ "port": "Seleziona il dispositivo"
+ },
+ "title": "Dispositivo"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "port": "Percorso del dispositivo USB"
+ },
+ "title": "Percorso"
+ },
+ "user": {
+ "data": {
+ "type": "Tipo di connessione"
+ },
+ "title": "Selezionare il tipo di connessione"
+ }
}
},
"options": {
diff --git a/homeassistant/components/dsmr/translations/nl.json b/homeassistant/components/dsmr/translations/nl.json
index ba31fa36fd2..ed6171279f1 100644
--- a/homeassistant/components/dsmr/translations/nl.json
+++ b/homeassistant/components/dsmr/translations/nl.json
@@ -1,15 +1,47 @@
{
"config": {
"abort": {
- "already_configured": "Apparaat is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd",
+ "cannot_communicate": "Kon niet verbinden.",
+ "cannot_connect": "Kan geen verbinding maken"
},
"error": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "cannot_communicate": "Kon niet verbinden.",
+ "cannot_connect": "Kan geen verbinding maken",
"one": "Leeg",
"other": "Leeg"
},
"step": {
"one": "Leeg",
- "other": "Leeg"
+ "other": "Leeg",
+ "setup_network": {
+ "data": {
+ "dsmr_version": "Selecteer DSMR-versie",
+ "host": "Host",
+ "port": "Poort"
+ },
+ "title": "Selecteer verbindingsadres"
+ },
+ "setup_serial": {
+ "data": {
+ "dsmr_version": "Selecteer DSMR-versie",
+ "port": "Selecteer apparaat"
+ },
+ "title": "Apparaat"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "port": "USB-apparaatpad"
+ },
+ "title": "Pad"
+ },
+ "user": {
+ "data": {
+ "type": "Verbindingstype"
+ },
+ "title": "Selecteer verbindingstype"
+ }
}
},
"options": {
diff --git a/homeassistant/components/dsmr/translations/no.json b/homeassistant/components/dsmr/translations/no.json
index e51520bf730..ef9d2798f2b 100644
--- a/homeassistant/components/dsmr/translations/no.json
+++ b/homeassistant/components/dsmr/translations/no.json
@@ -1,7 +1,43 @@
{
"config": {
"abort": {
- "already_configured": "Enheten er allerede konfigurert"
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_communicate": "Kunne ikke kommunisere",
+ "cannot_connect": "Tilkobling mislyktes"
+ },
+ "error": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_communicate": "Kunne ikke kommunisere",
+ "cannot_connect": "Tilkobling mislyktes"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "dsmr_version": "Velg DSMR-versjon",
+ "host": "Vert",
+ "port": "Port"
+ },
+ "title": "Velg tilkoblingsadresse"
+ },
+ "setup_serial": {
+ "data": {
+ "dsmr_version": "Velg DSMR-versjon",
+ "port": "Velg enhet"
+ },
+ "title": "Enhet"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "port": "USB enhetsbane"
+ },
+ "title": "Bane"
+ },
+ "user": {
+ "data": {
+ "type": "Tilkoblingstype"
+ },
+ "title": "Velg tilkoblingstype"
+ }
}
},
"options": {
diff --git a/homeassistant/components/dsmr/translations/ru.json b/homeassistant/components/dsmr/translations/ru.json
index 3bf0cf9f06f..0ee5d273156 100644
--- a/homeassistant/components/dsmr/translations/ru.json
+++ b/homeassistant/components/dsmr/translations/ru.json
@@ -1,7 +1,43 @@
{
"config": {
"abort": {
- "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "cannot_communicate": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u0432\u044f\u0437\u0430\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."
+ },
+ "error": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "cannot_communicate": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u0432\u044f\u0437\u0430\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."
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "dsmr_version": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0435\u0440\u0441\u0438\u044e DSMR",
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f"
+ },
+ "setup_serial": {
+ "data": {
+ "dsmr_version": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0435\u0440\u0441\u0438\u044e DSMR",
+ "port": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e"
+ },
+ "title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "port": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
+ },
+ "title": "\u041f\u0443\u0442\u044c"
+ },
+ "user": {
+ "data": {
+ "type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f"
+ },
+ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f"
+ }
}
},
"options": {
diff --git a/homeassistant/components/dsmr/translations/zh-Hant.json b/homeassistant/components/dsmr/translations/zh-Hant.json
index 52e77cd3520..9d95685a87f 100644
--- a/homeassistant/components/dsmr/translations/zh-Hant.json
+++ b/homeassistant/components/dsmr/translations/zh-Hant.json
@@ -1,7 +1,43 @@
{
"config": {
"abort": {
- "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_communicate": "\u901a\u8a0a\u5931\u6557",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "error": {
+ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_communicate": "\u901a\u8a0a\u5931\u6557",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "dsmr_version": "\u9078\u64c7 DSM \u7248\u672c",
+ "host": "\u4e3b\u6a5f\u7aef",
+ "port": "\u901a\u8a0a\u57e0"
+ },
+ "title": "\u9078\u64c7\u9023\u7dda\u4f4d\u5740"
+ },
+ "setup_serial": {
+ "data": {
+ "dsmr_version": "\u9078\u64c7 DSM \u7248\u672c",
+ "port": "\u9078\u64c7\u88dd\u7f6e"
+ },
+ "title": "\u88dd\u7f6e"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "port": "USB \u88dd\u7f6e\u8def\u5f91"
+ },
+ "title": "\u8def\u5f91"
+ },
+ "user": {
+ "data": {
+ "type": "\u9023\u7dda\u985e\u578b"
+ },
+ "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b"
+ }
}
},
"options": {
diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py
index daf6b9eb950..d403f84e9b9 100644
--- a/homeassistant/components/dsmr_reader/definitions.py
+++ b/homeassistant/components/dsmr_reader/definitions.py
@@ -268,6 +268,12 @@ DEFINITIONS = {
"icon": "mdi:currency-eur",
"unit": CURRENCY_EURO,
},
+ "dsmr/day-consumption/fixed_cost": {
+ "name": "Current day fixed cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
"dsmr/meter-stats/dsmr_version": {
"name": "DSMR version",
"enable_default": True,
diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py
index 27475990de0..4e095955818 100644
--- a/homeassistant/components/dte_energy_bridge/sensor.py
+++ b/homeassistant/components/dte_energy_bridge/sensor.py
@@ -4,7 +4,11 @@ import logging
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.components.sensor import (
+ PLATFORM_SCHEMA,
+ STATE_CLASS_MEASUREMENT,
+ SensorEntity,
+)
from homeassistant.const import CONF_NAME, HTTP_OK
import homeassistant.helpers.config_validation as cv
@@ -41,6 +45,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class DteEnergyBridgeSensor(SensorEntity):
"""Implementation of the DTE Energy Bridge sensors."""
+ _attr_state_class = STATE_CLASS_MEASUREMENT
+
def __init__(self, ip_address, name, version):
"""Initialize the sensor."""
self._version = version
diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py
index 17e9b6d9a37..482b92f768e 100644
--- a/homeassistant/components/dunehd/media_player.py
+++ b/homeassistant/components/dunehd/media_player.py
@@ -31,7 +31,7 @@ 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, StateType
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN
@@ -100,7 +100,7 @@ class DuneHDPlayerEntity(MediaPlayerEntity):
return True
@property
- def state(self) -> StateType:
+ def state(self) -> str | None:
"""Return player state."""
state = STATE_OFF
if "playback_position" in self._state:
diff --git a/homeassistant/components/dunehd/translations/he.json b/homeassistant/components/dunehd/translations/he.json
new file mode 100644
index 00000000000..5cf4da123b5
--- /dev/null
+++ b/homeassistant/components/dunehd/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/eafm/translations/de.json b/homeassistant/components/eafm/translations/de.json
index 9bb9fda51bf..46185acc11b 100644
--- a/homeassistant/components/eafm/translations/de.json
+++ b/homeassistant/components/eafm/translations/de.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "no_stations": "Keine Hochwassermessstellen gefunden."
},
"step": {
"user": {
diff --git a/homeassistant/components/eafm/translations/he.json b/homeassistant/components/eafm/translations/he.json
new file mode 100644
index 00000000000..d32dde2f055
--- /dev/null
+++ b/homeassistant/components/eafm/translations/he.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "station": "\u05ea\u05d7\u05e0\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py
index 9593fc0e497..b81a5e6bef6 100644
--- a/homeassistant/components/ecobee/binary_sensor.py
+++ b/homeassistant/components/ecobee/binary_sensor.py
@@ -4,7 +4,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
)
-from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER
+from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -67,17 +67,11 @@ class EcobeeBinarySensor(BinarySensorEntity):
f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat"
)
except KeyError:
- _LOGGER.error(
- "Model number for ecobee thermostat %s not recognized. "
- "Please visit this link and provide the following information: "
- "https://github.com/home-assistant/core/issues/27172 "
- "Unrecognized model number: %s",
- thermostat["name"],
- thermostat["modelNumber"],
- )
+ # Ecobee model is not in our list
+ model = None
break
- if identifier is not None and model is not None:
+ if identifier is not None:
return {
"identifiers": {(DOMAIN, identifier)},
"name": self.sensor_name,
@@ -86,6 +80,12 @@ class EcobeeBinarySensor(BinarySensorEntity):
}
return None
+ @property
+ def available(self):
+ """Return true if device is available."""
+ thermostat = self.data.ecobee.get_thermostat(self.index)
+ return thermostat["runtime"]["connected"]
+
@property
def is_on(self):
"""Return the status of the sensor."""
diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py
index 9e9e2eff1c8..eeac7ddb224 100644
--- a/homeassistant/components/ecobee/climate.py
+++ b/homeassistant/components/ecobee/climate.py
@@ -176,10 +176,23 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the ecobee thermostat."""
data = hass.data[DOMAIN]
+ entities = []
- devices = [Thermostat(data, index) for index in range(len(data.ecobee.thermostats))]
+ for index in range(len(data.ecobee.thermostats)):
+ thermostat = data.ecobee.get_thermostat(index)
+ if not thermostat["modelNumber"] in ECOBEE_MODEL_TO_NAME:
+ _LOGGER.error(
+ "Model number for ecobee thermostat %s not recognized. "
+ "Please visit this link to open a new issue: "
+ "https://github.com/home-assistant/core/issues "
+ "and include the following information: "
+ "Unrecognized model number: %s",
+ thermostat["name"],
+ thermostat["modelNumber"],
+ )
+ entities.append(Thermostat(data, index, thermostat))
- async_add_entities(devices, True)
+ async_add_entities(entities, True)
platform = entity_platform.async_get_current_platform()
@@ -187,7 +200,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Create a vacation on the target thermostat."""
entity_id = service.data[ATTR_ENTITY_ID]
- for thermostat in devices:
+ for thermostat in entities:
if thermostat.entity_id == entity_id:
thermostat.create_vacation(service.data)
thermostat.schedule_update_ha_state(True)
@@ -198,7 +211,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entity_id = service.data[ATTR_ENTITY_ID]
vacation_name = service.data[ATTR_VACATION_NAME]
- for thermostat in devices:
+ for thermostat in entities:
if thermostat.entity_id == entity_id:
thermostat.delete_vacation(vacation_name)
thermostat.schedule_update_ha_state(True)
@@ -211,10 +224,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if entity_id:
target_thermostats = [
- device for device in devices if device.entity_id in entity_id
+ entity for entity in entities if entity.entity_id in entity_id
]
else:
- target_thermostats = devices
+ target_thermostats = entities
for thermostat in target_thermostats:
thermostat.set_fan_min_on_time(str(fan_min_on_time))
@@ -228,10 +241,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if entity_id:
target_thermostats = [
- device for device in devices if device.entity_id in entity_id
+ entity for entity in entities if entity.entity_id in entity_id
]
else:
- target_thermostats = devices
+ target_thermostats = entities
for thermostat in target_thermostats:
thermostat.resume_program(resume_all)
@@ -291,11 +304,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class Thermostat(ClimateEntity):
"""A thermostat class for Ecobee."""
- def __init__(self, data, thermostat_index):
+ def __init__(self, data, thermostat_index, thermostat):
"""Initialize the thermostat."""
self.data = data
self.thermostat_index = thermostat_index
- self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index)
+ self.thermostat = thermostat
self._name = self.thermostat["name"]
self.vacation = None
self._last_active_hvac_mode = HVAC_MODE_HEAT_COOL
@@ -358,15 +371,8 @@ class Thermostat(ClimateEntity):
try:
model = f"{ECOBEE_MODEL_TO_NAME[self.thermostat['modelNumber']]} Thermostat"
except KeyError:
- _LOGGER.error(
- "Model number for ecobee thermostat %s not recognized. "
- "Please visit this link and provide the following information: "
- "https://github.com/home-assistant/core/issues/27172 "
- "Unrecognized model number: %s",
- self.name,
- self.thermostat["modelNumber"],
- )
- return None
+ # Ecobee model is not in our list
+ model = None
return {
"identifiers": {(DOMAIN, self.thermostat["identifier"])},
diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py
index 5067d5080cb..984609c2f22 100644
--- a/homeassistant/components/ecobee/humidifier.py
+++ b/homeassistant/components/ecobee/humidifier.py
@@ -10,7 +10,7 @@ from homeassistant.components.humidifier.const import (
SUPPORT_MODES,
)
-from .const import DOMAIN
+from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER
SCAN_INTERVAL = timedelta(minutes=3)
@@ -43,6 +43,37 @@ class EcobeeHumidifier(HumidifierEntity):
self.update_without_throttle = False
+ @property
+ def name(self):
+ """Return the name of the humidifier."""
+ return self._name
+
+ @property
+ def unique_id(self):
+ """Return unique_id for humidifier."""
+ return f"{self.thermostat['identifier']}"
+
+ @property
+ def device_info(self):
+ """Return device information for the ecobee humidifier."""
+ try:
+ model = f"{ECOBEE_MODEL_TO_NAME[self.thermostat['modelNumber']]} Thermostat"
+ except KeyError:
+ # Ecobee model is not in our list
+ model = None
+
+ return {
+ "identifiers": {(DOMAIN, self.thermostat["identifier"])},
+ "name": self.name,
+ "manufacturer": MANUFACTURER,
+ "model": model,
+ }
+
+ @property
+ def available(self):
+ """Return if device is available."""
+ return self.thermostat["runtime"]["connected"]
+
async def async_update(self):
"""Get the latest state from the thermostat."""
if self.update_without_throttle:
@@ -84,11 +115,6 @@ class EcobeeHumidifier(HumidifierEntity):
"""Return the current mode, e.g., off, auto, manual."""
return self.thermostat["settings"]["humidifierMode"]
- @property
- def name(self):
- """Return the name of the ecobee thermostat."""
- return self._name
-
@property
def supported_features(self):
"""Return the list of supported features."""
diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py
index 5abe809e59d..275db46ab0a 100644
--- a/homeassistant/components/ecobee/sensor.py
+++ b/homeassistant/components/ecobee/sensor.py
@@ -9,7 +9,7 @@ from homeassistant.const import (
TEMP_FAHRENHEIT,
)
-from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER
+from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER
SENSOR_TYPES = {
"temperature": ["Temperature", TEMP_FAHRENHEIT],
@@ -79,14 +79,8 @@ class EcobeeSensor(SensorEntity):
f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat"
)
except KeyError:
- _LOGGER.error(
- "Model number for ecobee thermostat %s not recognized. "
- "Please visit this link and provide the following information: "
- "https://github.com/home-assistant/core/issues/27172 "
- "Unrecognized model number: %s",
- thermostat["name"],
- thermostat["modelNumber"],
- )
+ # Ecobee model is not in our list
+ model = None
break
if identifier is not None and model is not None:
@@ -98,6 +92,12 @@ class EcobeeSensor(SensorEntity):
}
return None
+ @property
+ def available(self):
+ """Return true if device is available."""
+ thermostat = self.data.ecobee.get_thermostat(self.index)
+ return thermostat["runtime"]["connected"]
+
@property
def device_class(self):
"""Return the device class of the sensor."""
diff --git a/homeassistant/components/ecobee/translations/he.json b/homeassistant/components/ecobee/translations/he.json
new file mode 100644
index 00000000000..bafbfda24e8
--- /dev/null
+++ b/homeassistant/components/ecobee/translations/he.json
@@ -0,0 +1,14 @@
+{
+ "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": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py
index 7774a6648a5..8e3de2be90a 100644
--- a/homeassistant/components/ecobee/weather.py
+++ b/homeassistant/components/ecobee/weather.py
@@ -12,11 +12,11 @@ from homeassistant.components.weather import (
ATTR_FORECAST_WIND_SPEED,
WeatherEntity,
)
-from homeassistant.const import TEMP_FAHRENHEIT
+from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_FAHRENHEIT
from homeassistant.util import dt as dt_util
+from homeassistant.util.pressure import convert as pressure_convert
from .const import (
- _LOGGER,
DOMAIN,
ECOBEE_MODEL_TO_NAME,
ECOBEE_WEATHER_SYMBOL_TO_HASS,
@@ -71,15 +71,8 @@ class EcobeeWeather(WeatherEntity):
try:
model = f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat"
except KeyError:
- _LOGGER.error(
- "Model number for ecobee thermostat %s not recognized. "
- "Please visit this link and provide the following information: "
- "https://github.com/home-assistant/core/issues/27172 "
- "Unrecognized model number: %s",
- thermostat["name"],
- thermostat["modelNumber"],
- )
- return None
+ # Ecobee model is not in our list
+ model = None
return {
"identifiers": {(DOMAIN, thermostat["identifier"])},
@@ -113,7 +106,11 @@ class EcobeeWeather(WeatherEntity):
def pressure(self):
"""Return the pressure."""
try:
- return int(self.get_forecast(0, "pressure"))
+ pressure = self.get_forecast(0, "pressure")
+ if not self.hass.config.units.is_metric:
+ pressure = pressure_convert(pressure, PRESSURE_HPA, PRESSURE_INHG)
+ return round(pressure, 2)
+ return round(pressure)
except ValueError:
return None
diff --git a/homeassistant/components/econet/translations/de.json b/homeassistant/components/econet/translations/de.json
index 854d61f1790..3b487d9f0e8 100644
--- a/homeassistant/components/econet/translations/de.json
+++ b/homeassistant/components/econet/translations/de.json
@@ -14,7 +14,8 @@
"data": {
"email": "E-Mail",
"password": "Passwort"
- }
+ },
+ "title": "Rheem EcoNet-Konto einrichten"
}
}
}
diff --git a/homeassistant/components/econet/translations/he.json b/homeassistant/components/econet/translations/he.json
new file mode 100644
index 00000000000..a881cd42615
--- /dev/null
+++ b/homeassistant/components/econet/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\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"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u05d3\u05d5\u05d0\"\u05dc",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ee_brightbox/device_tracker.py b/homeassistant/components/ee_brightbox/device_tracker.py
index 64f81023796..f29eaf6f948 100644
--- a/homeassistant/components/ee_brightbox/device_tracker.py
+++ b/homeassistant/components/ee_brightbox/device_tracker.py
@@ -1,6 +1,7 @@
"""Support for EE Brightbox router."""
import logging
+# pylint: disable=import-error
from eebrightbox import EEBrightBox, EEBrightBoxException
import voluptuous as vol
diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py
index 67c195da3e6..4e16cd1087f 100644
--- a/homeassistant/components/eight_sleep/__init__.py
+++ b/homeassistant/components/eight_sleep/__init__.py
@@ -11,10 +11,10 @@ from homeassistant.const import (
CONF_PASSWORD,
CONF_SENSORS,
CONF_USERNAME,
- EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import callback
from homeassistant.helpers import discovery
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
@@ -29,7 +29,6 @@ _LOGGER = logging.getLogger(__name__)
CONF_PARTNER = "partner"
DATA_EIGHT = "eight_sleep"
-DEFAULT_PARTNER = False
DOMAIN = "eight_sleep"
HEAT_ENTITY = "heat"
@@ -86,12 +85,15 @@ SERVICE_EIGHT_SCHEMA = vol.Schema(
CONFIG_SCHEMA = vol.Schema(
{
- DOMAIN: vol.Schema(
- {
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_PARTNER, default=DEFAULT_PARTNER): cv.boolean,
- }
+ 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,
+ }
+ ),
)
},
extra=vol.ALLOW_EXTRA,
@@ -104,7 +106,6 @@ async def async_setup(hass, config):
conf = config.get(DOMAIN)
user = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
- partner = conf.get(CONF_PARTNER)
if hass.config.time_zone is None:
_LOGGER.error("Timezone is not set in Home Assistant")
@@ -112,7 +113,7 @@ async def async_setup(hass, config):
timezone = str(hass.config.time_zone)
- eight = EightSleep(user, password, timezone, partner, None, hass.loop)
+ eight = EightSleep(user, password, timezone, async_get_clientsession(hass))
hass.data[DATA_EIGHT] = eight
@@ -190,12 +191,6 @@ async def async_setup(hass, config):
DOMAIN, SERVICE_HEAT_SET, async_service_handler, schema=SERVICE_EIGHT_SCHEMA
)
- async def stop_eight(event):
- """Handle stopping eight api session."""
- await eight.stop()
-
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_eight)
-
return True
diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py
index 803b20383b6..d8a763c2e54 100644
--- a/homeassistant/components/eight_sleep/binary_sensor.py
+++ b/homeassistant/components/eight_sleep/binary_sensor.py
@@ -1,7 +1,10 @@
"""Support for Eight Sleep binary sensors."""
import logging
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_OCCUPANCY,
+ BinarySensorEntity,
+)
from . import CONF_BINARY_SENSORS, DATA_EIGHT, NAME_MAP, EightSleepHeatEntity
@@ -34,13 +37,15 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorEntity):
self._sensor = sensor
self._mapped_name = NAME_MAP.get(self._sensor, self._sensor)
- self._name = f"{name} {self._mapped_name}"
self._state = None
self._side = self._sensor.split("_")[0]
self._userid = self._eight.fetch_userid(self._side)
self._usrobj = self._eight.users[self._userid]
+ self._attr_name = f"{name} {self._mapped_name}"
+ self._attr_device_class = DEVICE_CLASS_OCCUPANCY
+
_LOGGER.debug(
"Presence Sensor: %s, Side: %s, User: %s",
self._sensor,
@@ -48,11 +53,6 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorEntity):
self._userid,
)
- @property
- def name(self):
- """Return the name of the sensor, if any."""
- return self._name
-
@property
def is_on(self):
"""Return true if the binary sensor is on."""
diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json
index d0f86d5a5e4..1c3944a985e 100644
--- a/homeassistant/components/eight_sleep/manifest.json
+++ b/homeassistant/components/eight_sleep/manifest.json
@@ -2,7 +2,7 @@
"domain": "eight_sleep",
"name": "Eight Sleep",
"documentation": "https://www.home-assistant.io/integrations/eight_sleep",
- "requirements": ["pyeight==0.1.5"],
+ "requirements": ["pyeight==0.1.9"],
"codeowners": ["@mezz64"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py
index 46060fe23fb..7f7e432c5f0 100644
--- a/homeassistant/components/elgato/light.py
+++ b/homeassistant/components/elgato/light.py
@@ -77,8 +77,8 @@ class ElgatoLight(LightEntity):
min_mired = 153
max_mired = 285
- self._attr_max_mired = max_mired
- self._attr_min_mired = min_mired
+ self._attr_max_mireds = max_mired
+ self._attr_min_mireds = min_mired
self._attr_name = info.display_name or info.product_name
self._attr_supported_color_modes = supported_color_modes
self._attr_unique_id = info.serial_number
diff --git a/homeassistant/components/elgato/translations/de.json b/homeassistant/components/elgato/translations/de.json
index cb7fb18f003..95bb2609d84 100644
--- a/homeassistant/components/elgato/translations/de.json
+++ b/homeassistant/components/elgato/translations/de.json
@@ -7,14 +7,14 @@
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
},
- "flow_title": "Elgato Key Light: {serial_number}",
+ "flow_title": "{serial_number}",
"step": {
"user": {
"data": {
"host": "Host",
"port": "Port"
},
- "description": "Richten dein Elgato Key Light f\u00fcr die Integration mit Home Assistant ein."
+ "description": "Richte deinen Elgato Key Light f\u00fcr die Integration mit Home Assistant ein."
},
"zeroconf_confirm": {
"description": "M\u00f6chtest du das Elgato Key Light mit der Seriennummer \"{serial_number} \" zu Home Assistant hinzuf\u00fcgen?",
diff --git a/homeassistant/components/elgato/translations/he.json b/homeassistant/components/elgato/translations/he.json
new file mode 100644
index 00000000000..e0d0e50be74
--- /dev/null
+++ b/homeassistant/components/elgato/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",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{serial_number}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elgato/translations/nl.json b/homeassistant/components/elgato/translations/nl.json
index 5fa47d30a10..1605f4577ee 100644
--- a/homeassistant/components/elgato/translations/nl.json
+++ b/homeassistant/components/elgato/translations/nl.json
@@ -18,7 +18,7 @@
},
"zeroconf_confirm": {
"description": "Wilt u de Elgato Key Light met serienummer `{serial_number}` toevoegen aan Home Assistant?",
- "title": "Elgato Key Light apparaat ontdekt"
+ "title": "Elgato Light apparaat ontdekt"
}
}
}
diff --git a/homeassistant/components/elgato/translations/pl.json b/homeassistant/components/elgato/translations/pl.json
index 17b8522be35..94764903d10 100644
--- a/homeassistant/components/elgato/translations/pl.json
+++ b/homeassistant/components/elgato/translations/pl.json
@@ -14,7 +14,7 @@
"host": "Nazwa hosta lub adres IP",
"port": "Port"
},
- "description": "Konfiguracja Elgato Light w celu integracji z Home Assistantem."
+ "description": "Skonfiguruj Elgato Light, aby zintegrowa\u0107 go z Home Assistantem."
},
"zeroconf_confirm": {
"description": "Czy chcesz doda\u0107 urz\u0105dzenie Elgato Light o numerze seryjnym `{serial_number}` do Home Assistanta?",
diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py
index a4d812850f7..253913b3779 100644
--- a/homeassistant/components/eliqonline/sensor.py
+++ b/homeassistant/components/eliqonline/sensor.py
@@ -6,7 +6,11 @@ import logging
import eliqonline
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.components.sensor import (
+ PLATFORM_SCHEMA,
+ STATE_CLASS_MEASUREMENT,
+ SensorEntity,
+)
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, POWER_WATT
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
@@ -54,6 +58,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class EliqSensor(SensorEntity):
"""Implementation of an ELIQ Online sensor."""
+ _attr_state_class = STATE_CLASS_MEASUREMENT
+
def __init__(self, api, channel_id, name):
"""Initialize the sensor."""
self._name = name
diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py
index ff2f2533d24..6a96a73de22 100644
--- a/homeassistant/components/elkm1/__init__.py
+++ b/homeassistant/components/elkm1/__init__.py
@@ -195,7 +195,7 @@ def _async_find_matching_config_entry(hass, prefix):
return entry
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Elk-M1 Control from a config entry."""
conf = entry.data
diff --git a/homeassistant/components/elkm1/translations/he.json b/homeassistant/components/elkm1/translations/he.json
index ac90b3264ea..e85bab17ac0 100644
--- a/homeassistant/components/elkm1/translations/he.json
+++ b/homeassistant/components/elkm1/translations/he.json
@@ -1,9 +1,15 @@
{
"config": {
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
"user": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py
index 516f38d64c2..69c8b907b72 100644
--- a/homeassistant/components/emonitor/__init__.py
+++ b/homeassistant/components/emonitor/__init__.py
@@ -19,7 +19,7 @@ DEFAULT_UPDATE_RATE = 60
PLATFORMS = ["sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up SiteSage Emonitor from a config entry."""
session = aiohttp_client.async_get_clientsession(hass)
diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py
index 2b9d715ba51..1dca3f2d89d 100644
--- a/homeassistant/components/emonitor/sensor.py
+++ b/homeassistant/components/emonitor/sensor.py
@@ -37,6 +37,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
"""Representation of an Emonitor power sensor entity."""
+ _attr_device_class = DEVICE_CLASS_POWER
+ _attr_unit_of_measurement = POWER_WATT
+
def __init__(self, coordinator: DataUpdateCoordinator, channel_number: int) -> None:
"""Initialize the channel sensor."""
self.channel_number = channel_number
@@ -62,16 +65,6 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
"""Name of the sensor."""
return self.channel_data.label
- @property
- def unit_of_measurement(self) -> str:
- """Return the unit of measurement."""
- return POWER_WATT
-
- @property
- def device_class(self) -> str:
- """Device class of the sensor."""
- return DEVICE_CLASS_POWER
-
def _paired_attr(self, attr_name: str) -> float:
"""Cumulative attributes for channel and paired channel."""
attr_val = getattr(self.channel_data, attr_name)
diff --git a/homeassistant/components/emonitor/translations/de.json b/homeassistant/components/emonitor/translations/de.json
index 6abbe1b2b27..c36f7a5ae77 100644
--- a/homeassistant/components/emonitor/translations/de.json
+++ b/homeassistant/components/emonitor/translations/de.json
@@ -7,7 +7,12 @@
"cannot_connect": "Verbindung fehlgeschlagen",
"unknown": "Unerwarteter Fehler"
},
+ "flow_title": "{name}",
"step": {
+ "confirm": {
+ "description": "M\u00f6chten Sie {name} ({host}) einrichten?",
+ "title": "Einrichtung SiteSage Emonitor"
+ },
"user": {
"data": {
"host": "Host"
diff --git a/homeassistant/components/emonitor/translations/he.json b/homeassistant/components/emonitor/translations/he.json
new file mode 100644
index 00000000000..4ec15aa12cb
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/he.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?"
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/translations/hu.json b/homeassistant/components/emonitor/translations/hu.json
index 2d7d4218e7d..575e2a91d44 100644
--- a/homeassistant/components/emonitor/translations/hu.json
+++ b/homeassistant/components/emonitor/translations/hu.json
@@ -7,7 +7,7 @@
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "SiteSage {name}",
+ "flow_title": "{name}",
"step": {
"confirm": {
"description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?",
diff --git a/homeassistant/components/emulated_roku/translations/he.json b/homeassistant/components/emulated_roku/translations/he.json
new file mode 100644
index 00000000000..92608aacfa2
--- /dev/null
+++ b/homeassistant/components/emulated_roku/translations/he.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host_ip": "\u05db\u05ea\u05d5\u05d1\u05ea IP \u05de\u05d0\u05e8\u05d7\u05ea",
+ "name": "\u05e9\u05dd"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enocean/translations/de.json b/homeassistant/components/enocean/translations/de.json
index ea9858f470e..a8e4e2c7f84 100644
--- a/homeassistant/components/enocean/translations/de.json
+++ b/homeassistant/components/enocean/translations/de.json
@@ -1,7 +1,25 @@
{
"config": {
"abort": {
+ "invalid_dongle_path": "Ung\u00fcltiger Dongle-Pfad",
"single_instance_allowed": "Schon konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "error": {
+ "invalid_dongle_path": "Kein g\u00fcltiger Dongle unter diesem Pfad gefunden"
+ },
+ "step": {
+ "detect": {
+ "data": {
+ "path": "USB-Dongle-Pfad"
+ },
+ "title": "W\u00e4hlen Sie den Pfad zu Ihrem ENOcean-Dongle"
+ },
+ "manual": {
+ "data": {
+ "path": "USB-Dongle-Pfad"
+ },
+ "title": "Geben Sie den Pfad zu Ihrem ENOcean-Dongle ein"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/enocean/translations/he.json b/homeassistant/components/enocean/translations/he.json
new file mode 100644
index 00000000000..d0c3523da94
--- /dev/null
+++ b/homeassistant/components/enocean/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/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py
index 89803d32351..7a1de25e242 100644
--- a/homeassistant/components/enphase_envoy/const.py
+++ b/homeassistant/components/enphase_envoy/const.py
@@ -1,6 +1,7 @@
"""The enphase_envoy component."""
+from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT
DOMAIN = "enphase_envoy"
@@ -12,19 +13,21 @@ COORDINATOR = "coordinator"
NAME = "name"
SENSORS = {
- "production": ("Current Energy Production", POWER_WATT),
- "daily_production": ("Today's Energy Production", ENERGY_WATT_HOUR),
+ "production": ("Current Energy Production", POWER_WATT, STATE_CLASS_MEASUREMENT),
+ "daily_production": ("Today's Energy Production", ENERGY_WATT_HOUR, None),
"seven_days_production": (
"Last Seven Days Energy Production",
ENERGY_WATT_HOUR,
+ None,
),
- "lifetime_production": ("Lifetime Energy Production", ENERGY_WATT_HOUR),
- "consumption": ("Current Energy Consumption", POWER_WATT),
- "daily_consumption": ("Today's Energy Consumption", ENERGY_WATT_HOUR),
+ "lifetime_production": ("Lifetime Energy Production", ENERGY_WATT_HOUR, None),
+ "consumption": ("Current Energy Consumption", POWER_WATT, STATE_CLASS_MEASUREMENT),
+ "daily_consumption": ("Today's Energy Consumption", ENERGY_WATT_HOUR, None),
"seven_days_consumption": (
"Last Seven Days Energy Consumption",
ENERGY_WATT_HOUR,
+ None,
),
- "lifetime_consumption": ("Lifetime Energy Consumption", ENERGY_WATT_HOUR),
- "inverters": ("Inverter", POWER_WATT),
+ "lifetime_consumption": ("Lifetime Energy Consumption", ENERGY_WATT_HOUR, None),
+ "inverters": ("Inverter", POWER_WATT, STATE_CLASS_MEASUREMENT),
}
diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py
index 050a497f69e..5ccb540efd0 100644
--- a/homeassistant/components/enphase_envoy/sensor.py
+++ b/homeassistant/components/enphase_envoy/sensor.py
@@ -74,6 +74,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
config_entry.unique_id,
serial_number,
SENSORS[condition][1],
+ SENSORS[condition][2],
coordinator,
)
)
@@ -91,6 +92,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
config_entry.unique_id,
None,
SENSORS[condition][1],
+ SENSORS[condition][2],
coordinator,
)
)
@@ -109,6 +111,7 @@ class Envoy(CoordinatorEntity, SensorEntity):
device_serial_number,
serial_number,
unit,
+ state_class,
coordinator,
):
"""Initialize Envoy entity."""
@@ -118,6 +121,7 @@ class Envoy(CoordinatorEntity, SensorEntity):
self._device_name = device_name
self._device_serial_number = device_serial_number
self._unit_of_measurement = unit
+ self._attr_state_class = state_class
super().__init__(coordinator)
diff --git a/homeassistant/components/enphase_envoy/translations/he.json b/homeassistant/components/enphase_envoy/translations/he.json
new file mode 100644
index 00000000000..94741f81ff9
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/he.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "flow_title": "{serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/hu.json b/homeassistant/components/enphase_envoy/translations/hu.json
index caef6a32c86..3449489bd87 100644
--- a/homeassistant/components/enphase_envoy/translations/hu.json
+++ b/homeassistant/components/enphase_envoy/translations/hu.json
@@ -8,7 +8,7 @@
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "Envoy {serial} ({host})",
+ "flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json
index ad522be9321..e2a5175211c 100644
--- a/homeassistant/components/entur_public_transport/manifest.json
+++ b/homeassistant/components/entur_public_transport/manifest.json
@@ -2,7 +2,7 @@
"domain": "entur_public_transport",
"name": "Entur",
"documentation": "https://www.home-assistant.io/integrations/entur_public_transport",
- "requirements": ["enturclient==0.2.1"],
+ "requirements": ["enturclient==0.2.2"],
"codeowners": ["@hfurubotten"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py
index 9abbc33bc93..a4a8a02cee9 100644
--- a/homeassistant/components/environment_canada/weather.py
+++ b/homeassistant/components/environment_canada/weather.py
@@ -202,16 +202,17 @@ def get_forecast(ec_data, forecast_type):
ATTR_FORECAST_TEMP_LOW: int(half_days[1]["temperature"]),
}
)
+ half_days = half_days[2:]
else:
today.update(
{
+ ATTR_FORECAST_TEMP: None,
ATTR_FORECAST_TEMP_LOW: int(half_days[0]["temperature"]),
- ATTR_FORECAST_TEMP: int(half_days[1]["temperature"]),
}
)
+ half_days = half_days[1:]
forecast_array.append(today)
- half_days = half_days[2:]
for day, high, low in zip(range(1, 6), range(0, 9, 2), range(1, 10, 2)):
forecast_array.append(
@@ -231,19 +232,20 @@ def get_forecast(ec_data, forecast_type):
)
elif forecast_type == "hourly":
- hours = ec_data.hourly_forecasts
- for hour in range(0, 24):
+ for hour in ec_data.hourly_forecasts:
forecast_array.append(
{
- ATTR_FORECAST_TIME: dt.as_local(
- datetime.datetime.strptime(hours[hour]["period"], "%Y%m%d%H%M")
- ).isoformat(),
- ATTR_FORECAST_TEMP: int(hours[hour]["temperature"]),
+ ATTR_FORECAST_TIME: datetime.datetime.strptime(
+ hour["period"], "%Y%m%d%H%M%S"
+ )
+ .replace(tzinfo=dt.UTC)
+ .isoformat(),
+ ATTR_FORECAST_TEMP: int(hour["temperature"]),
ATTR_FORECAST_CONDITION: icon_code_to_condition(
- int(hours[hour]["icon_code"])
+ int(hour["icon_code"])
),
ATTR_FORECAST_PRECIPITATION_PROBABILITY: int(
- hours[hour]["precip_probability"]
+ hour["precip_probability"]
),
}
)
diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py
index 1982731b9ef..e60df7dc8bc 100644
--- a/homeassistant/components/epson/__init__.py
+++ b/homeassistant/components/epson/__init__.py
@@ -41,7 +41,7 @@ async def validate_projector(
return epson_proj
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up epson from a config entry."""
projector = await validate_projector(
hass=hass,
diff --git a/homeassistant/components/epson/translations/he.json b/homeassistant/components/epson/translations/he.json
new file mode 100644
index 00000000000..33660936e12
--- /dev/null
+++ b/homeassistant/components/epson/translations/he.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "name": "\u05e9\u05dd"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py
index 607af8cc47d..aa7da100505 100644
--- a/homeassistant/components/esphome/__init__.py
+++ b/homeassistant/components/esphome/__init__.py
@@ -2,14 +2,16 @@
from __future__ import annotations
import asyncio
+from dataclasses import dataclass, field
import functools
import logging
import math
-from typing import Callable
+from typing import Generic, TypeVar
from aioesphomeapi import (
APIClient,
APIConnectionError,
+ APIVersion,
DeviceInfo as EsphomeDeviceInfo,
EntityInfo,
EntityState,
@@ -48,14 +50,60 @@ from .entry_data import RuntimeEntryData
DOMAIN = "esphome"
_LOGGER = logging.getLogger(__name__)
+_T = TypeVar("_T")
STORAGE_VERSION = 1
+@dataclass
+class DomainData:
+ """Define a class that stores global esphome data in hass.data[DOMAIN]."""
+
+ _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict)
+ _stores: dict[str, Store] = field(default_factory=dict)
+
+ def get_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData:
+ """Return the runtime entry data associated with this config entry.
+
+ Raises KeyError if the entry isn't loaded yet.
+ """
+ return self._entry_datas[entry.entry_id]
+
+ def set_entry_data(self, entry: ConfigEntry, entry_data: RuntimeEntryData) -> None:
+ """Set the runtime entry data associated with this config entry."""
+ if entry.entry_id in self._entry_datas:
+ raise ValueError("Entry data for this entry is already set")
+ self._entry_datas[entry.entry_id] = entry_data
+
+ def pop_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData:
+ """Pop the runtime entry data instance associated with this config entry."""
+ return self._entry_datas.pop(entry.entry_id)
+
+ def is_entry_loaded(self, entry: ConfigEntry) -> bool:
+ """Check whether the given entry is loaded."""
+ return entry.entry_id in self._entry_datas
+
+ def get_or_create_store(self, hass: HomeAssistant, entry: ConfigEntry) -> Store:
+ """Get or create a Store instance for the given config entry."""
+ return self._stores.setdefault(
+ entry.entry_id,
+ Store(
+ hass, STORAGE_VERSION, f"esphome.{entry.entry_id}", encoder=JSONEncoder
+ ),
+ )
+
+ @classmethod
+ def get(cls: type[_T], hass: HomeAssistant) -> _T:
+ """Get the global DomainData instance stored in hass.data."""
+ # Don't use setdefault - this is a hot code path
+ if DOMAIN in hass.data:
+ return hass.data[DOMAIN]
+ ret = hass.data[DOMAIN] = cls()
+ return ret
+
+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the esphome component."""
- hass.data.setdefault(DOMAIN, {})
-
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
password = entry.data[CONF_PASSWORD]
@@ -72,13 +120,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
zeroconf_instance=zeroconf_instance,
)
- # Store client in per-config-entry hass.data
- store = Store(
- hass, STORAGE_VERSION, f"esphome.{entry.entry_id}", encoder=JSONEncoder
- )
- entry_data = hass.data[DOMAIN][entry.entry_id] = RuntimeEntryData(
- client=cli, entry_id=entry.entry_id, store=store
+ domain_data = DomainData.get(hass)
+ entry_data = RuntimeEntryData(
+ client=cli,
+ entry_id=entry.entry_id,
+ store=domain_data.get_or_create_store(hass, entry),
)
+ domain_data.set_entry_data(entry, entry_data)
async def on_stop(event: Event) -> None:
"""Cleanup the socket client on HA stop."""
@@ -205,6 +253,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
nonlocal device_id
try:
entry_data.device_info = await cli.device_info()
+ entry_data.api_version = cli.api_version
entry_data.available = True
device_id = await _async_setup_device_registry(
hass, entry, entry_data.device_info
@@ -283,7 +332,11 @@ class ReconnectLogic(RecordUpdateListener):
@property
def _entry_data(self) -> RuntimeEntryData | None:
- return self._hass.data[DOMAIN].get(self._entry.entry_id)
+ domain_data = DomainData.get(self._hass)
+ try:
+ return domain_data.get_entry_data(self._entry)
+ except KeyError:
+ return None
async def _on_disconnect(self):
"""Log and issue callbacks when disconnecting."""
@@ -379,7 +432,7 @@ class ReconnectLogic(RecordUpdateListener):
return False
# Check if the entry got removed or disabled, in which case we shouldn't reconnect
- if self._entry.entry_id not in self._hass.data[DOMAIN]:
+ if not DomainData.get(self._hass).is_entry_loaded(self._entry):
# When removing/disconnecting manually
return
@@ -552,7 +605,7 @@ async def _register_service(
"example": "['Example text', 'Another example']",
"selector": {"object": {}},
},
- }[arg.type_]
+ }[arg.type]
schema[vol.Required(arg.name)] = metadata["validator"]
fields[arg.name] = {
"name": arg.name,
@@ -612,7 +665,8 @@ async def _cleanup_instance(
hass: HomeAssistant, entry: ConfigEntry
) -> RuntimeEntryData:
"""Cleanup the esphome client if it exists."""
- data: RuntimeEntryData = hass.data[DOMAIN].pop(entry.entry_id)
+ domain_data = DomainData.get(hass)
+ data = domain_data.pop_entry_data(entry)
for disconnect_cb in data.disconnect_callbacks:
disconnect_cb()
for cleanup_callback in data.cleanup_callbacks:
@@ -629,6 +683,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
+async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
+ """Remove an esphome config entry."""
+ await DomainData.get(hass).get_or_create_store(hass, entry).async_remove()
+
+
async def platform_async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
@@ -644,7 +703,7 @@ async def platform_async_setup_entry(
This method is in charge of receiving, distributing and storing
info and state updates.
"""
- entry_data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id]
+ entry_data: RuntimeEntryData = DomainData.get(hass).get_entry_data(entry)
entry_data.info[component_key] = {}
entry_data.old_info[component_key] = {}
entry_data.state[component_key] = {}
@@ -665,7 +724,7 @@ async def platform_async_setup_entry(
old_infos.pop(info.key)
else:
# Create new entity
- entity = entity_type(entry.entry_id, component_key, info.key)
+ entity = entity_type(entry_data, component_key, info.key)
add_entities.append(entity)
new_infos[info.key] = info
@@ -721,38 +780,33 @@ def esphome_state_property(func):
return _wrapper
-class EsphomeEnumMapper:
+class EsphomeEnumMapper(Generic[_T]):
"""Helper class to convert between hass and esphome enum values."""
- def __init__(self, func: Callable[[], dict[int, str]]) -> None:
+ def __init__(self, mapping: dict[_T, str]) -> None:
"""Construct a EsphomeEnumMapper."""
- self._func = func
+ # Add none mapping
+ mapping = {None: None, **mapping}
+ self._mapping = mapping
+ self._inverse: dict[str, _T] = {v: k for k, v in mapping.items()}
- def from_esphome(self, value: int) -> str:
+ def from_esphome(self, value: _T | None) -> str | None:
"""Convert from an esphome int representation to a hass string."""
- return self._func()[value]
+ return self._mapping[value]
- def from_hass(self, value: str) -> int:
+ def from_hass(self, value: str) -> _T:
"""Convert from a hass string to a esphome int representation."""
- inverse = {v: k for k, v in self._func().items()}
- return inverse[value]
-
-
-def esphome_map_enum(func: Callable[[], dict[int, str]]):
- """Map esphome int enum values to hass string constants.
-
- This class has to be used as a decorator. This ensures the aioesphomeapi
- import is only happening at runtime.
- """
- return EsphomeEnumMapper(func)
+ return self._inverse[value]
class EsphomeBaseEntity(Entity):
"""Define a base esphome entity."""
- def __init__(self, entry_id: str, component_key: str, key: int) -> None:
+ def __init__(
+ self, entry_data: RuntimeEntryData, component_key: str, key: int
+ ) -> None:
"""Initialize."""
- self._entry_id = entry_id
+ self._entry_data = entry_data
self._component_key = component_key
self._key = key
@@ -788,8 +842,12 @@ class EsphomeBaseEntity(Entity):
self.async_write_ha_state()
@property
- def _entry_data(self) -> RuntimeEntryData:
- return self.hass.data[DOMAIN][self._entry_id]
+ def _entry_id(self) -> str:
+ return self._entry_data.entry_id
+
+ @property
+ def _api_version(self) -> APIVersion:
+ return self._entry_data.api_version
@property
def _static_info(self) -> EntityInfo:
diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py
index 6b553de1a13..7afd89bf9be 100644
--- a/homeassistant/components/esphome/camera.py
+++ b/homeassistant/components/esphome/camera.py
@@ -32,10 +32,10 @@ async def async_setup_entry(
class EsphomeCamera(Camera, EsphomeBaseEntity):
"""A camera implementation for ESPHome."""
- def __init__(self, entry_id: str, component_key: str, key: int) -> None:
+ def __init__(self, *args, **kwargs) -> None:
"""Initialize."""
Camera.__init__(self)
- EsphomeBaseEntity.__init__(self, entry_id, component_key, key)
+ EsphomeBaseEntity.__init__(self, *args, **kwargs)
self._image_cond = asyncio.Condition()
@property
diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py
index 5d21d495ec2..f7ebccc8434 100644
--- a/homeassistant/components/esphome/climate.py
+++ b/homeassistant/components/esphome/climate.py
@@ -6,6 +6,7 @@ from aioesphomeapi import (
ClimateFanMode,
ClimateInfo,
ClimateMode,
+ ClimatePreset,
ClimateState,
ClimateSwingMode,
)
@@ -30,14 +31,21 @@ from homeassistant.components.climate.const import (
FAN_MIDDLE,
FAN_OFF,
FAN_ON,
+ HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
+ PRESET_ACTIVITY,
PRESET_AWAY,
+ PRESET_BOOST,
+ PRESET_COMFORT,
+ PRESET_ECO,
PRESET_HOME,
+ PRESET_NONE,
+ PRESET_SLEEP,
SUPPORT_FAN_MODE,
SUPPORT_PRESET_MODE,
SUPPORT_SWING_MODE,
@@ -58,7 +66,7 @@ from homeassistant.const import (
from . import (
EsphomeEntity,
- esphome_map_enum,
+ EsphomeEnumMapper,
esphome_state_property,
platform_async_setup_entry,
)
@@ -77,21 +85,19 @@ async def async_setup_entry(hass, entry, async_add_entities):
)
-@esphome_map_enum
-def _climate_modes():
- return {
+_CLIMATE_MODES: EsphomeEnumMapper[ClimateMode] = EsphomeEnumMapper(
+ {
ClimateMode.OFF: HVAC_MODE_OFF,
- ClimateMode.AUTO: HVAC_MODE_HEAT_COOL,
+ ClimateMode.HEAT_COOL: HVAC_MODE_HEAT_COOL,
ClimateMode.COOL: HVAC_MODE_COOL,
ClimateMode.HEAT: HVAC_MODE_HEAT,
ClimateMode.FAN_ONLY: HVAC_MODE_FAN_ONLY,
ClimateMode.DRY: HVAC_MODE_DRY,
+ ClimateMode.AUTO: HVAC_MODE_AUTO,
}
-
-
-@esphome_map_enum
-def _climate_actions():
- return {
+)
+_CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction] = EsphomeEnumMapper(
+ {
ClimateAction.OFF: CURRENT_HVAC_OFF,
ClimateAction.COOLING: CURRENT_HVAC_COOL,
ClimateAction.HEATING: CURRENT_HVAC_HEAT,
@@ -99,11 +105,9 @@ def _climate_actions():
ClimateAction.DRYING: CURRENT_HVAC_DRY,
ClimateAction.FAN: CURRENT_HVAC_FAN,
}
-
-
-@esphome_map_enum
-def _fan_modes():
- return {
+)
+_FAN_MODES: EsphomeEnumMapper[ClimateFanMode] = EsphomeEnumMapper(
+ {
ClimateFanMode.ON: FAN_ON,
ClimateFanMode.OFF: FAN_OFF,
ClimateFanMode.AUTO: FAN_AUTO,
@@ -114,16 +118,27 @@ def _fan_modes():
ClimateFanMode.FOCUS: FAN_FOCUS,
ClimateFanMode.DIFFUSE: FAN_DIFFUSE,
}
-
-
-@esphome_map_enum
-def _swing_modes():
- return {
+)
+_SWING_MODES: EsphomeEnumMapper[ClimateSwingMode] = EsphomeEnumMapper(
+ {
ClimateSwingMode.OFF: SWING_OFF,
ClimateSwingMode.BOTH: SWING_BOTH,
ClimateSwingMode.VERTICAL: SWING_VERTICAL,
ClimateSwingMode.HORIZONTAL: SWING_HORIZONTAL,
}
+)
+_PRESETS: EsphomeEnumMapper[ClimatePreset] = EsphomeEnumMapper(
+ {
+ ClimatePreset.NONE: PRESET_NONE,
+ ClimatePreset.HOME: PRESET_HOME,
+ ClimatePreset.AWAY: PRESET_AWAY,
+ ClimatePreset.BOOST: PRESET_BOOST,
+ ClimatePreset.COMFORT: PRESET_COMFORT,
+ ClimatePreset.ECO: PRESET_ECO,
+ ClimatePreset.SLEEP: PRESET_SLEEP,
+ ClimatePreset.ACTIVITY: PRESET_ACTIVITY,
+ }
+)
class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
@@ -156,28 +171,31 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
def hvac_modes(self) -> list[str]:
"""Return the list of available operation modes."""
return [
- _climate_modes.from_esphome(mode)
+ _CLIMATE_MODES.from_esphome(mode)
for mode in self._static_info.supported_modes
]
@property
- def fan_modes(self):
+ def fan_modes(self) -> list[str]:
"""Return the list of available fan modes."""
return [
- _fan_modes.from_esphome(mode)
+ _FAN_MODES.from_esphome(mode)
for mode in self._static_info.supported_fan_modes
- ]
+ ] + self._static_info.supported_custom_fan_modes
@property
- def preset_modes(self):
+ def preset_modes(self) -> list[str]:
"""Return preset modes."""
- return [PRESET_AWAY, PRESET_HOME] if self._static_info.supports_away else []
+ return [
+ _PRESETS.from_esphome(preset)
+ for preset in self._static_info.supported_presets_compat(self._api_version)
+ ] + self._static_info.supported_custom_presets
@property
def swing_modes(self):
"""Return the list of available swing modes."""
return [
- _swing_modes.from_esphome(mode)
+ _SWING_MODES.from_esphome(mode)
for mode in self._static_info.supported_swing_modes
]
@@ -205,7 +223,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
features |= SUPPORT_TARGET_TEMPERATURE_RANGE
else:
features |= SUPPORT_TARGET_TEMPERATURE
- if self._static_info.supports_away:
+ if self.preset_modes:
features |= SUPPORT_PRESET_MODE
if self._static_info.supported_fan_modes:
features |= SUPPORT_FAN_MODE
@@ -219,7 +237,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
@esphome_state_property
def hvac_mode(self) -> str | None:
"""Return current operation ie. heat, cool, idle."""
- return _climate_modes.from_esphome(self._state.mode)
+ return _CLIMATE_MODES.from_esphome(self._state.mode)
@esphome_state_property
def hvac_action(self) -> str | None:
@@ -227,22 +245,26 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
# HA has no support feature field for hvac_action
if not self._static_info.supports_action:
return None
- return _climate_actions.from_esphome(self._state.action)
+ return _CLIMATE_ACTIONS.from_esphome(self._state.action)
@esphome_state_property
- def fan_mode(self):
+ def fan_mode(self) -> str | None:
"""Return current fan setting."""
- return _fan_modes.from_esphome(self._state.fan_mode)
+ return self._state.custom_fan_mode or _FAN_MODES.from_esphome(
+ self._state.fan_mode
+ )
@esphome_state_property
- def preset_mode(self):
+ def preset_mode(self) -> str | None:
"""Return current preset mode."""
- return PRESET_AWAY if self._state.away else PRESET_HOME
+ return self._state.custom_preset or _PRESETS.from_esphome(
+ self._state.preset_compat(self._api_version)
+ )
@esphome_state_property
- def swing_mode(self):
+ def swing_mode(self) -> str | None:
"""Return current swing mode."""
- return _swing_modes.from_esphome(self._state.swing_mode)
+ return _SWING_MODES.from_esphome(self._state.swing_mode)
@esphome_state_property
def current_temperature(self) -> float | None:
@@ -268,7 +290,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
"""Set new target temperature (and operation mode if set)."""
data = {"key": self._static_info.key}
if ATTR_HVAC_MODE in kwargs:
- data["mode"] = _climate_modes.from_hass(kwargs[ATTR_HVAC_MODE])
+ data["mode"] = _CLIMATE_MODES.from_hass(kwargs[ATTR_HVAC_MODE])
if ATTR_TEMPERATURE in kwargs:
data["target_temperature"] = kwargs[ATTR_TEMPERATURE]
if ATTR_TARGET_TEMP_LOW in kwargs:
@@ -280,22 +302,29 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target operation mode."""
await self._client.climate_command(
- key=self._static_info.key, mode=_climate_modes.from_hass(hvac_mode)
+ key=self._static_info.key, mode=_CLIMATE_MODES.from_hass(hvac_mode)
)
- async def async_set_preset_mode(self, preset_mode):
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set preset mode."""
- away = preset_mode == PRESET_AWAY
- await self._client.climate_command(key=self._static_info.key, away=away)
+ kwargs = {}
+ if preset_mode in self._static_info.supported_custom_presets:
+ kwargs["custom_preset"] = preset_mode
+ else:
+ kwargs["preset"] = _PRESETS.from_hass(preset_mode)
+ await self._client.climate_command(key=self._static_info.key, **kwargs)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new fan mode."""
- await self._client.climate_command(
- key=self._static_info.key, fan_mode=_fan_modes.from_hass(fan_mode)
- )
+ kwargs = {}
+ if fan_mode in self._static_info.supported_custom_fan_modes:
+ kwargs["custom_fan_mode"] = fan_mode
+ else:
+ kwargs["fan_mode"] = _FAN_MODES.from_hass(fan_mode)
+ await self._client.climate_command(key=self._static_info.key, **kwargs)
async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set new swing mode."""
await self._client.climate_command(
- key=self._static_info.key, swing_mode=_swing_modes.from_hass(swing_mode)
+ key=self._static_info.key, swing_mode=_SWING_MODES.from_hass(swing_mode)
)
diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py
index e31fa202a39..38e44b12508 100644
--- a/homeassistant/components/esphome/config_flow.py
+++ b/homeassistant/components/esphome/config_flow.py
@@ -12,8 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
from homeassistant.core import callback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
-from . import DOMAIN
-from .entry_data import RuntimeEntryData
+from . import DOMAIN, DomainData
class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -104,9 +103,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
]:
# Is this address or IP address already configured?
already_configured = True
- elif entry.entry_id in self.hass.data.get(DOMAIN, {}):
+ elif DomainData.get(self.hass).is_entry_loaded(entry):
# Does a config entry with this name already exist?
- data: RuntimeEntryData = self.hass.data[DOMAIN][entry.entry_id]
+ data = DomainData.get(self.hass).get_entry_data(entry)
# Node names are unique in the network
if data.device_info is not None:
diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py
index 3f4bd29198c..3064f827d7f 100644
--- a/homeassistant/components/esphome/cover.py
+++ b/homeassistant/components/esphome/cover.py
@@ -74,7 +74,7 @@ class EsphomeCover(EsphomeEntity, CoverEntity):
def is_closed(self) -> bool | None:
"""Return if the cover is closed or not."""
# Check closed state with api version due to a protocol change
- return self._state.is_closed(self._client.api_version)
+ return self._state.is_closed(self._api_version)
@esphome_state_property
def is_opening(self) -> bool:
diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py
index c5d36e3a68d..f60d7cfefb5 100644
--- a/homeassistant/components/esphome/entry_data.py
+++ b/homeassistant/components/esphome/entry_data.py
@@ -2,10 +2,12 @@
from __future__ import annotations
import asyncio
+from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Callable
from aioesphomeapi import (
COMPONENT_TYPE_TO_INFO,
+ APIVersion,
BinarySensorInfo,
CameraInfo,
ClimateInfo,
@@ -15,12 +17,12 @@ from aioesphomeapi import (
EntityState,
FanInfo,
LightInfo,
+ NumberInfo,
SensorInfo,
SwitchInfo,
TextSensorInfo,
UserService,
)
-import attr
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
@@ -40,37 +42,38 @@ INFO_TYPE_TO_PLATFORM = {
CoverInfo: "cover",
FanInfo: "fan",
LightInfo: "light",
+ NumberInfo: "number",
SensorInfo: "sensor",
SwitchInfo: "switch",
TextSensorInfo: "sensor",
}
-@attr.s
+@dataclass
class RuntimeEntryData:
"""Store runtime data for esphome config entries."""
- _storage_contents: dict | None = None
-
- entry_id: str = attr.ib()
- client: APIClient = attr.ib()
- store: Store = attr.ib()
- state: dict[str, dict[str, Any]] = attr.ib(factory=dict)
- info: dict[str, dict[str, Any]] = attr.ib(factory=dict)
+ entry_id: str
+ client: APIClient
+ store: Store
+ state: dict[str, dict[str, Any]] = field(default_factory=dict)
+ info: dict[str, dict[str, Any]] = field(default_factory=dict)
# A second list of EntityInfo objects
# This is necessary for when an entity is being removed. HA requires
# some static info to be accessible during removal (unique_id, maybe others)
# If an entity can't find anything in the info array, it will look for info here.
- old_info: dict[str, dict[str, Any]] = attr.ib(factory=dict)
+ old_info: dict[str, dict[str, Any]] = field(default_factory=dict)
- services: dict[int, UserService] = attr.ib(factory=dict)
- available: bool = attr.ib(default=False)
- device_info: DeviceInfo | None = attr.ib(default=None)
- cleanup_callbacks: list[Callable[[], None]] = attr.ib(factory=list)
- disconnect_callbacks: list[Callable[[], None]] = attr.ib(factory=list)
- loaded_platforms: set[str] = attr.ib(factory=set)
- platform_load_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock)
+ services: dict[int, UserService] = field(default_factory=dict)
+ available: bool = False
+ device_info: DeviceInfo | None = None
+ api_version: APIVersion = field(default_factory=APIVersion)
+ cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list)
+ disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list)
+ loaded_platforms: set[str] = field(default_factory=set)
+ platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
+ _storage_contents: dict | None = None
@callback
def async_update_entity(
@@ -138,16 +141,15 @@ class RuntimeEntryData:
return [], []
self._storage_contents = restored.copy()
- self.device_info = _attr_obj_from_dict(
- DeviceInfo, **restored.pop("device_info")
- )
+ self.device_info = DeviceInfo.from_dict(restored.pop("device_info"))
+ self.api_version = APIVersion.from_dict(restored.pop("api_version", {}))
infos = []
for comp_type, restored_infos in restored.items():
if comp_type not in COMPONENT_TYPE_TO_INFO:
continue
for info in restored_infos:
cls = COMPONENT_TYPE_TO_INFO[comp_type]
- infos.append(_attr_obj_from_dict(cls, **info))
+ infos.append(cls.from_dict(info))
services = []
for service in restored.get("services", []):
services.append(UserService.from_dict(service))
@@ -155,10 +157,14 @@ class RuntimeEntryData:
async def async_save_to_store(self) -> None:
"""Generate dynamic data to store and save it to the filesystem."""
- store_data = {"device_info": attr.asdict(self.device_info), "services": []}
+ store_data = {
+ "device_info": self.device_info.to_dict(),
+ "services": [],
+ "api_version": self.api_version.to_dict(),
+ }
for comp_type, infos in self.info.items():
- store_data[comp_type] = [attr.asdict(info) for info in infos.values()]
+ store_data[comp_type] = [info.to_dict() for info in infos.values()]
for service in self.services.values():
store_data["services"].append(service.to_dict())
@@ -170,7 +176,3 @@ class RuntimeEntryData:
return store_data
self.store.async_delay_save(_memorized_storage, SAVE_DELAY)
-
-
-def _attr_obj_from_dict(cls, **kwargs):
- return cls(**{key: kwargs[key] for key in attr.fields_dict(cls) if key in kwargs})
diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py
index 5272cdef5f1..e02958d5885 100644
--- a/homeassistant/components/esphome/fan.py
+++ b/homeassistant/components/esphome/fan.py
@@ -24,7 +24,7 @@ from homeassistant.util.percentage import (
from . import (
EsphomeEntity,
- esphome_map_enum,
+ EsphomeEnumMapper,
esphome_state_property,
platform_async_setup_entry,
)
@@ -47,12 +47,12 @@ async def async_setup_entry(
)
-@esphome_map_enum
-def _fan_directions():
- return {
+_FAN_DIRECTIONS: EsphomeEnumMapper[FanDirection] = EsphomeEnumMapper(
+ {
FanDirection.FORWARD: DIRECTION_FORWARD,
FanDirection.REVERSE: DIRECTION_REVERSE,
}
+)
class EsphomeFan(EsphomeEntity, FanEntity):
@@ -68,7 +68,7 @@ class EsphomeFan(EsphomeEntity, FanEntity):
@property
def _supports_speed_levels(self) -> bool:
- api_version = self._client.api_version
+ api_version = self._api_version
return api_version.major == 1 and api_version.minor > 3
async def async_set_percentage(self, percentage: int) -> None:
@@ -115,7 +115,7 @@ class EsphomeFan(EsphomeEntity, FanEntity):
async def async_set_direction(self, direction: str):
"""Set direction of the fan."""
await self._client.fan_command(
- key=self._static_info.key, direction=_fan_directions.from_hass(direction)
+ key=self._static_info.key, direction=_FAN_DIRECTIONS.from_hass(direction)
)
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
@@ -149,18 +149,18 @@ class EsphomeFan(EsphomeEntity, FanEntity):
return self._static_info.supported_speed_levels
@esphome_state_property
- def oscillating(self) -> None:
+ def oscillating(self) -> bool | None:
"""Return the oscillation state."""
if not self._static_info.supports_oscillation:
return None
return self._state.oscillating
@esphome_state_property
- def current_direction(self) -> None:
+ def current_direction(self) -> str | None:
"""Return the current fan direction."""
if not self._static_info.supports_direction:
return None
- return _fan_directions.from_esphome(self._state.direction)
+ return _FAN_DIRECTIONS.from_esphome(self._state.direction)
@property
def supported_features(self) -> int:
diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json
index 592ca616d04..e69299a4a43 100644
--- a/homeassistant/components/esphome/manifest.json
+++ b/homeassistant/components/esphome/manifest.json
@@ -3,9 +3,9 @@
"name": "ESPHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/esphome",
- "requirements": ["aioesphomeapi==2.8.0"],
+ "requirements": ["aioesphomeapi==4.0.1"],
"zeroconf": ["_esphomelib._tcp.local."],
- "codeowners": ["@OttoWinter"],
+ "codeowners": ["@OttoWinter", "@jesserockz"],
"after_dependencies": ["zeroconf", "tag"],
"iot_class": "local_push"
}
diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py
new file mode 100644
index 00000000000..08b31e91b79
--- /dev/null
+++ b/homeassistant/components/esphome/number.py
@@ -0,0 +1,85 @@
+"""Support for esphome numbers."""
+from __future__ import annotations
+
+import math
+
+from aioesphomeapi import NumberInfo, NumberState
+import voluptuous as vol
+
+from homeassistant.components.number import NumberEntity
+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 . import EsphomeEntity, esphome_state_property, platform_async_setup_entry
+
+ICON_SCHEMA = vol.Schema(cv.icon)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up esphome numbers based on a config entry."""
+ await platform_async_setup_entry(
+ hass,
+ entry,
+ async_add_entities,
+ component_key="number",
+ info_type=NumberInfo,
+ entity_type=EsphomeNumber,
+ state_type=NumberState,
+ )
+
+
+# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
+# pylint: disable=invalid-overridden-method
+
+
+class EsphomeNumber(EsphomeEntity, NumberEntity):
+ """A number implementation for esphome."""
+
+ @property
+ def _static_info(self) -> NumberInfo:
+ return super()._static_info
+
+ @property
+ def _state(self) -> NumberState | None:
+ return super()._state
+
+ @property
+ def icon(self) -> str | None:
+ """Return the icon."""
+ if not self._static_info.icon:
+ return None
+ return ICON_SCHEMA(self._static_info.icon)
+
+ @property
+ def min_value(self) -> float:
+ """Return the minimum value."""
+ return super()._static_info.min_value
+
+ @property
+ def max_value(self) -> float:
+ """Return the maximum value."""
+ return super()._static_info.max_value
+
+ @property
+ def step(self) -> float:
+ """Return the increment/decrement step."""
+ return super()._static_info.step
+
+ @esphome_state_property
+ def value(self) -> float:
+ """Return the state of the entity."""
+ if math.isnan(self._state.state):
+ return None
+ if self._state.missing_state:
+ return None
+ return self._state.state
+
+ async def async_set_value(self, value: float) -> None:
+ """Update the current value."""
+ await self._client.number_command(self._static_info.key, value)
diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py
index 7b905aad148..d3dce2dea1b 100644
--- a/homeassistant/components/esphome/sensor.py
+++ b/homeassistant/components/esphome/sensor.py
@@ -25,7 +25,7 @@ from homeassistant.util import dt
from . import (
EsphomeEntity,
- esphome_map_enum,
+ EsphomeEnumMapper,
esphome_state_property,
platform_async_setup_entry,
)
@@ -61,12 +61,12 @@ async def async_setup_entry(
# pylint: disable=invalid-overridden-method
-@esphome_map_enum
-def _state_classes():
- return {
+_STATE_CLASSES: EsphomeEnumMapper[SensorStateClass] = EsphomeEnumMapper(
+ {
SensorStateClass.NONE: None,
SensorStateClass.MEASUREMENT: STATE_CLASS_MEASUREMENT,
}
+)
class EsphomeSensor(EsphomeEntity, SensorEntity):
@@ -122,7 +122,7 @@ class EsphomeSensor(EsphomeEntity, SensorEntity):
"""Return the state class of this entity."""
if not self._static_info.state_class:
return None
- return _state_classes.from_esphome(self._static_info.state_class)
+ return _STATE_CLASSES.from_esphome(self._static_info.state_class)
class EsphomeTextSensor(EsphomeEntity, SensorEntity):
diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json
index fdaea452c45..c82afc78851 100644
--- a/homeassistant/components/esphome/translations/de.json
+++ b/homeassistant/components/esphome/translations/de.json
@@ -9,7 +9,7 @@
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, lege eine statische IP-Adresse fest: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
- "flow_title": "ESPHome: {name}",
+ "flow_title": "{name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/translations/he.json b/homeassistant/components/esphome/translations/he.json
index 648d007cc46..5c0f832ba4c 100644
--- a/homeassistant/components/esphome/translations/he.json
+++ b/homeassistant/components/esphome/translations/he.json
@@ -1,9 +1,24 @@
{
"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"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "flow_title": "{name}",
"step": {
"authenticate": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d4\u05d2\u05d3\u05e8\u05ea \u05d1\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc\u05da \u05e2\u05d1\u05d5\u05e8 {name}."
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
}
}
}
diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py
index 670e07a07dc..19dd5121d69 100644
--- a/homeassistant/components/ezviz/__init__.py
+++ b/homeassistant/components/ezviz/__init__.py
@@ -2,7 +2,8 @@
from datetime import timedelta
import logging
-from pyezviz.client import EzvizClient, HTTPError, InvalidURL, PyEzvizError
+from pyezviz.client import EzvizClient
+from pyezviz.exceptions import HTTPError, InvalidURL, PyEzvizError
from homeassistant.const import (
CONF_PASSWORD,
diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py
index 9d8db7fbb30..abfe06d8daf 100644
--- a/homeassistant/components/ezviz/binary_sensor.py
+++ b/homeassistant/components/ezviz/binary_sensor.py
@@ -15,7 +15,6 @@ async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Ezviz sensors based on a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
sensors = []
- sensor_type_name = "None"
for idx, camera in enumerate(coordinator.data):
for name in camera:
diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py
index 919ff5039b2..b09e5cdd901 100644
--- a/homeassistant/components/ezviz/camera.py
+++ b/homeassistant/components/ezviz/camera.py
@@ -1,29 +1,43 @@
"""Support ezviz camera devices."""
import asyncio
-from datetime import timedelta
import logging
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
+from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IGNORE, SOURCE_IMPORT
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME
-from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
+ ATTR_DIRECTION,
+ ATTR_ENABLE,
+ ATTR_LEVEL,
ATTR_SERIAL,
+ ATTR_SPEED,
+ ATTR_TYPE,
CONF_CAMERAS,
CONF_FFMPEG_ARGUMENTS,
DATA_COORDINATOR,
DEFAULT_CAMERA_USERNAME,
DEFAULT_FFMPEG_ARGUMENTS,
DEFAULT_RTSP_PORT,
+ DIR_DOWN,
+ DIR_LEFT,
+ DIR_RIGHT,
+ DIR_UP,
DOMAIN,
MANUFACTURER,
+ SERVICE_ALARM_SOUND,
+ SERVICE_ALARM_TRIGER,
+ SERVICE_DETECTION_SENSITIVITY,
+ SERVICE_PTZ,
+ SERVICE_WAKE_DEVICE,
)
CAMERA_SCHEMA = vol.Schema(
@@ -40,8 +54,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
_LOGGER = logging.getLogger(__name__)
-MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90)
-
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a Ezviz IP Camera from platform config."""
@@ -157,6 +169,46 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(camera_entities)
+ platform = entity_platform.current_platform.get()
+
+ platform.async_register_entity_service(
+ SERVICE_PTZ,
+ {
+ vol.Required(ATTR_DIRECTION): vol.In(
+ [DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT]
+ ),
+ vol.Required(ATTR_SPEED): cv.positive_int,
+ },
+ "perform_ptz",
+ )
+
+ platform.async_register_entity_service(
+ SERVICE_ALARM_TRIGER,
+ {
+ vol.Required(ATTR_ENABLE): cv.positive_int,
+ },
+ "perform_sound_alarm",
+ )
+
+ platform.async_register_entity_service(
+ SERVICE_WAKE_DEVICE, {}, "perform_wake_device"
+ )
+
+ platform.async_register_entity_service(
+ SERVICE_ALARM_SOUND,
+ {vol.Required(ATTR_LEVEL): cv.positive_int},
+ "perform_alarm_sound",
+ )
+
+ platform.async_register_entity_service(
+ SERVICE_DETECTION_SENSITIVITY,
+ {
+ vol.Required(ATTR_LEVEL): cv.positive_int,
+ vol.Required(ATTR_TYPE): cv.positive_int,
+ },
+ "perform_set_alarm_detection_sensibility",
+ )
+
class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity):
"""An implementation of a Ezviz security camera."""
@@ -232,6 +284,22 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity):
"""Camera Motion Detection Status."""
return self.coordinator.data[self._idx]["alarm_notify"]
+ def enable_motion_detection(self):
+ """Enable motion detection in camera."""
+ try:
+ self.coordinator.ezviz_client.set_camera_defence(self._serial, 1)
+
+ except InvalidHost as err:
+ raise InvalidHost("Error enabling motion detection") from err
+
+ def disable_motion_detection(self):
+ """Disable motion detection."""
+ try:
+ self.coordinator.ezviz_client.set_camera_defence(self._serial, 0)
+
+ except InvalidHost as err:
+ raise InvalidHost("Error disabling motion detection") from err
+
@property
def unique_id(self):
"""Return the name of this camera."""
@@ -271,3 +339,49 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity):
self._rtsp_stream = rtsp_stream_source
return rtsp_stream_source
return None
+
+ def perform_ptz(self, direction, speed):
+ """Perform a PTZ action on the camera."""
+ _LOGGER.debug("PTZ action '%s' on %s", direction, self._name)
+ try:
+ self.coordinator.ezviz_client.ptz_control(
+ str(direction).upper(), self._serial, "START", speed
+ )
+ self.coordinator.ezviz_client.ptz_control(
+ str(direction).upper(), self._serial, "STOP", speed
+ )
+
+ except HTTPError as err:
+ raise HTTPError("Cannot perform PTZ") from err
+
+ def perform_sound_alarm(self, enable):
+ """Sound the alarm on a camera."""
+ try:
+ self.coordinator.ezviz_client.sound_alarm(self._serial, enable)
+ except HTTPError as err:
+ raise HTTPError("Cannot sound alarm") from err
+
+ def perform_wake_device(self):
+ """Basically wakes the camera by querying the device."""
+ try:
+ self.coordinator.ezviz_client.get_detection_sensibility(self._serial)
+ except (HTTPError, PyEzvizError) as err:
+ raise PyEzvizError("Cannot wake device") from err
+
+ def perform_alarm_sound(self, level):
+ """Enable/Disable movement sound alarm."""
+ try:
+ self.coordinator.ezviz_client.alarm_sound(self._serial, level, 1)
+ except HTTPError as err:
+ raise HTTPError(
+ "Cannot set alarm sound level for on movement detected"
+ ) from err
+
+ def perform_set_alarm_detection_sensibility(self, level, type_value):
+ """Set camera detection sensibility level service."""
+ try:
+ self.coordinator.ezviz_client.detection_sensibility(
+ self._serial, level, type_value
+ )
+ except (HTTPError, PyEzvizError) as err:
+ raise PyEzvizError("Cannot set detection sensitivity level") from err
diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py
index 6915d8fa8db..8f10e3f0698 100644
--- a/homeassistant/components/ezviz/config_flow.py
+++ b/homeassistant/components/ezviz/config_flow.py
@@ -1,8 +1,15 @@
"""Config flow for ezviz."""
import logging
-from pyezviz.client import EzvizClient, HTTPError, InvalidURL, PyEzvizError
-from pyezviz.test_cam_rtsp import AuthTestResultFailed, InvalidHost, TestRTSPAuth
+from pyezviz.client import EzvizClient
+from pyezviz.exceptions import (
+ AuthTestResultFailed,
+ HTTPError,
+ InvalidHost,
+ InvalidURL,
+ PyEzvizError,
+)
+from pyezviz.test_cam_rtsp import TestRTSPAuth
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, OptionsFlow
diff --git a/homeassistant/components/ezviz/const.py b/homeassistant/components/ezviz/const.py
index c307f0693f6..e3e2cae712c 100644
--- a/homeassistant/components/ezviz/const.py
+++ b/homeassistant/components/ezviz/const.py
@@ -6,29 +6,30 @@ MANUFACTURER = "Ezviz"
# Configuration
ATTR_SERIAL = "serial"
CONF_CAMERAS = "cameras"
-ATTR_SWITCH = "switch"
-ATTR_ENABLE = "enable"
-ATTR_DIRECTION = "direction"
-ATTR_SPEED = "speed"
-ATTR_LEVEL = "level"
-ATTR_TYPE = "type_value"
-DIR_UP = "up"
-DIR_DOWN = "down"
-DIR_LEFT = "left"
-DIR_RIGHT = "right"
-ATTR_LIGHT = "LIGHT"
-ATTR_SOUND = "SOUND"
-ATTR_INFRARED_LIGHT = "INFRARED_LIGHT"
-ATTR_PRIVACY = "PRIVACY"
-ATTR_SLEEP = "SLEEP"
-ATTR_MOBILE_TRACKING = "MOBILE_TRACKING"
-ATTR_TRACKING = "TRACKING"
CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
ATTR_HOME = "HOME_MODE"
ATTR_AWAY = "AWAY_MODE"
ATTR_TYPE_CLOUD = "EZVIZ_CLOUD_ACCOUNT"
ATTR_TYPE_CAMERA = "CAMERA_ACCOUNT"
+# Services data
+DIR_UP = "up"
+DIR_DOWN = "down"
+DIR_LEFT = "left"
+DIR_RIGHT = "right"
+ATTR_ENABLE = "enable"
+ATTR_DIRECTION = "direction"
+ATTR_SPEED = "speed"
+ATTR_LEVEL = "level"
+ATTR_TYPE = "type_value"
+
+# Service names
+SERVICE_PTZ = "ptz"
+SERVICE_ALARM_TRIGER = "sound_alarm"
+SERVICE_WAKE_DEVICE = "wake_device"
+SERVICE_ALARM_SOUND = "alarm_sound"
+SERVICE_DETECTION_SENSITIVITY = "set_alarm_detection_sensibility"
+
# Defaults
EU_URL = "apiieu.ezvizlife.com"
RUSSIA_URL = "apirus.ezvizru.com"
diff --git a/homeassistant/components/ezviz/coordinator.py b/homeassistant/components/ezviz/coordinator.py
index 2fc9f6c9f82..ad755edce12 100644
--- a/homeassistant/components/ezviz/coordinator.py
+++ b/homeassistant/components/ezviz/coordinator.py
@@ -3,7 +3,7 @@ from datetime import timedelta
import logging
from async_timeout import timeout
-from pyezviz.client import HTTPError, InvalidURL, PyEzvizError
+from pyezviz.exceptions import HTTPError, InvalidURL, PyEzvizError
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json
index 46abf8bc99a..d5a38b17755 100644
--- a/homeassistant/components/ezviz/manifest.json
+++ b/homeassistant/components/ezviz/manifest.json
@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/ezviz",
"dependencies": ["ffmpeg"],
"codeowners": ["@RenierM26", "@baqs"],
- "requirements": ["pyezviz==0.1.8.7"],
+ "requirements": ["pyezviz==0.1.8.9"],
"config_flow": true,
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py
index f4f9f6588f0..fc07db89509 100644
--- a/homeassistant/components/ezviz/sensor.py
+++ b/homeassistant/components/ezviz/sensor.py
@@ -15,7 +15,6 @@ async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Ezviz sensors based on a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
sensors = []
- sensor_type_name = "None"
for idx, camera in enumerate(coordinator.data):
for name in camera:
diff --git a/homeassistant/components/ezviz/services.yaml b/homeassistant/components/ezviz/services.yaml
new file mode 100644
index 00000000000..2635662e636
--- /dev/null
+++ b/homeassistant/components/ezviz/services.yaml
@@ -0,0 +1,111 @@
+alarm_sound:
+ name: Set warning sound level.
+ description: Set movement warning sound level.
+ target:
+ entity:
+ integration: ezviz
+ domain: camera
+ fields:
+ level:
+ name: Sound level
+ description: Sound level (2 is disabled, 1 intensive, 0 soft).
+ required: true
+ example: 0
+ default: 0
+ selector:
+ number:
+ min: 0
+ max: 2
+ step: 1
+ mode: box
+ptz:
+ name: PTZ
+ description: Moves the camera to the direction, with defined speed
+ target:
+ entity:
+ integration: ezviz
+ domain: camera
+ fields:
+ direction:
+ name: Direction
+ description: Direction to move camera (up, down, left, right).
+ required: true
+ example: "up"
+ default: "up"
+ selector:
+ select:
+ options:
+ - "up"
+ - "down"
+ - "left"
+ - "right"
+ speed:
+ name: Speed
+ description: Speed of movement (from 1 to 9).
+ required: true
+ example: 5
+ default: 5
+ selector:
+ number:
+ min: 1
+ max: 9
+ step: 1
+ mode: box
+set_alarm_detection_sensibility:
+ name: Detection sensitivity
+ description: Sets the detection sensibility level.
+ target:
+ entity:
+ integration: ezviz
+ domain: camera
+ fields:
+ level:
+ name: Sensitivity Level
+ 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
+ selector:
+ number:
+ min: 1
+ max: 100
+ step: 1
+ mode: box
+ type_value:
+ name: Detection type
+ description: 'Type of detection. Options : 0 - Camera or 3 - PIR Sensor Camera'
+ required: true
+ example: '0'
+ default: '0'
+ selector:
+ select:
+ options:
+ - '0'
+ - '3'
+sound_alarm:
+ name: Sound Alarm
+ description: Sounds the alarm on your camera.
+ target:
+ entity:
+ integration: ezviz
+ domain: camera
+ fields:
+ enable:
+ description: Enter 1 or 2 (1=disable, 2=enable).
+ required: true
+ example: 1
+ default: 1
+ selector:
+ number:
+ min: 1
+ max: 2
+ step: 1
+ mode: box
+wake_device:
+ name: Wake Camera
+ description: This can be used to wake the camera/device from hibernation.
+ target:
+ entity:
+ integration: ezviz
+ domain: camera
diff --git a/homeassistant/components/ezviz/translations/de.json b/homeassistant/components/ezviz/translations/de.json
index 184255bcbcc..ab860d44201 100644
--- a/homeassistant/components/ezviz/translations/de.json
+++ b/homeassistant/components/ezviz/translations/de.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_account": "Konto wurde bereits konfiguriert",
+ "ezviz_cloud_account_missing": "Ezviz-Cloud-Konto fehlt. Bitte konfigurieren Sie das Ezviz-Cloud-Konto neu",
"unknown": "Unerwarteter Fehler"
},
"error": {
@@ -42,6 +43,7 @@
"step": {
"init": {
"data": {
+ "ffmpeg_arguments": "An ffmpeg \u00fcbergebene Argumente f\u00fcr Kameras",
"timeout": "Anfrage-Timeout (Sekunden)"
}
}
diff --git a/homeassistant/components/ezviz/translations/he.json b/homeassistant/components/ezviz/translations/he.json
new file mode 100644
index 00000000000..e45d7b58600
--- /dev/null
+++ b/homeassistant/components/ezviz/translations/he.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_account": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd"
+ },
+ "flow_title": "{serial}",
+ "step": {
+ "confirm": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "user_custom_url": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d1\u05e7\u05e9\u05d4 (\u05e9\u05e0\u05d9\u05d5\u05ea)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py
index 56cf9ad13bc..c270a878d49 100644
--- a/homeassistant/components/faa_delays/__init__.py
+++ b/homeassistant/components/faa_delays/__init__.py
@@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["binary_sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up FAA Delays from a config entry."""
code = entry.data[CONF_ID]
diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py
index f484ca36b25..20a11fd89f1 100644
--- a/homeassistant/components/fan/__init__.py
+++ b/homeassistant/components/fan/__init__.py
@@ -9,12 +9,14 @@ from typing import final
import voluptuous as vol
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_ON,
)
+from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
@@ -204,14 +206,16 @@ async def async_setup(hass, config: dict):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
def _fan_native(method):
diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py
index f4611d353d5..ddf6a76d3c8 100644
--- a/homeassistant/components/fan/device_action.py
+++ b/homeassistant/components/fan/device_action.py
@@ -38,22 +38,12 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
if entry.domain != DOMAIN:
continue
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "turn_on",
- }
- )
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "turn_off",
- }
- )
+ base_action = {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+ actions += [{**base_action, CONF_TYPE: action} for action in ACTION_TYPES]
return actions
diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py
index 9aa9620ef72..56d9208b2d2 100644
--- a/homeassistant/components/fan/device_condition.py
+++ b/homeassistant/components/fan/device_condition.py
@@ -42,24 +42,14 @@ async def async_get_conditions(
if entry.domain != DOMAIN:
continue
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_on",
- }
- )
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_off",
- }
- )
+ base_condition = {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
+ conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES]
return conditions
diff --git a/homeassistant/components/fan/translations/he.json b/homeassistant/components/fan/translations/he.json
index e2081b7460e..63139b0fe34 100644
--- a/homeassistant/components/fan/translations/he.json
+++ b/homeassistant/components/fan/translations/he.json
@@ -1,4 +1,10 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9",
+ "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc"
+ }
+ },
"state": {
"_": {
"off": "\u05db\u05d1\u05d5\u05d9",
diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py
index f54e3bc1fa2..454048a3737 100644
--- a/homeassistant/components/fireservicerota/switch.py
+++ b/homeassistant/components/fireservicerota/switch.py
@@ -98,7 +98,7 @@ class ResponseSwitch(SwitchEntity):
return attr
async def async_turn_on(self, **kwargs) -> None:
- """Send Acknowlegde response status."""
+ """Send Acknowledge response status."""
await self.async_set_response(True)
async def async_turn_off(self, **kwargs) -> None:
diff --git a/homeassistant/components/fireservicerota/translations/he.json b/homeassistant/components/fireservicerota/translations/he.json
new file mode 100644
index 00000000000..61dee20d1ce
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/he.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "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"
+ },
+ "create_entry": {
+ "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "url": "\u05d0\u05ea\u05e8 \u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/he.json b/homeassistant/components/firmata/translations/he.json
new file mode 100644
index 00000000000..0a2ba64dbef
--- /dev/null
+++ b/homeassistant/components/firmata/translations/he.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/it.json b/homeassistant/components/firmata/translations/it.json
index a4f6f9e7222..b7eb09c2cb8 100644
--- a/homeassistant/components/firmata/translations/it.json
+++ b/homeassistant/components/firmata/translations/it.json
@@ -2,6 +2,10 @@
"config": {
"abort": {
"cannot_connect": "Impossibile connettersi"
+ },
+ "step": {
+ "one": "Pi\u00f9",
+ "other": "Altri"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py
index ff9b737cd00..690cbe03cdd 100644
--- a/homeassistant/components/flick_electric/__init__.py
+++ b/homeassistant/components/flick_electric/__init__.py
@@ -24,7 +24,7 @@ CONF_ID_TOKEN = "id_token"
PLATFORMS = ["sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Flick Electric from a config entry."""
auth = HassFlickAuth(hass, entry)
diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py
index c523271716a..ab628e205c7 100644
--- a/homeassistant/components/flick_electric/sensor.py
+++ b/homeassistant/components/flick_electric/sensor.py
@@ -36,6 +36,8 @@ async def async_setup_entry(
class FlickPricingSensor(SensorEntity):
"""Entity object for Flick Electric sensor."""
+ _attr_unit_of_measurement = UNIT_NAME
+
def __init__(self, api: FlickAPI) -> None:
"""Entity object for Flick Electric sensor."""
self._api: FlickAPI = api
@@ -55,11 +57,6 @@ class FlickPricingSensor(SensorEntity):
"""Return the state of the sensor."""
return self._price.price
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return UNIT_NAME
-
@property
def extra_state_attributes(self):
"""Return the state attributes."""
diff --git a/homeassistant/components/flick_electric/translations/he.json b/homeassistant/components/flick_electric/translations/he.json
index 85688530711..658cdb97588 100644
--- a/homeassistant/components/flick_electric/translations/he.json
+++ b/homeassistant/components/flick_electric/translations/he.json
@@ -5,6 +5,7 @@
},
"error": {
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4"
},
"step": {
diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py
index 890f18ee3b7..734c4d9e766 100644
--- a/homeassistant/components/flo/__init__.py
+++ b/homeassistant/components/flo/__init__.py
@@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["binary_sensor", "sensor", "switch"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up flo from a config entry."""
session = async_get_clientsession(hass)
hass.data.setdefault(DOMAIN, {})
diff --git a/homeassistant/components/flo/config_flow.py b/homeassistant/components/flo/config_flow.py
index 038fc33777a..306ec945a3e 100644
--- a/homeassistant/components/flo/config_flow.py
+++ b/homeassistant/components/flo/config_flow.py
@@ -9,7 +9,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, LOGGER
-DATA_SCHEMA = vol.Schema({"username": str, "password": str})
+DATA_SCHEMA = vol.Schema({vol.Required("username"): str, vol.Required("password"): str})
async def validate_input(hass: core.HomeAssistant, data):
diff --git a/homeassistant/components/flo/translations/he.json b/homeassistant/components/flo/translations/he.json
new file mode 100644
index 00000000000..479d2f2f5e8
--- /dev/null
+++ b/homeassistant/components/flo/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "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",
+ "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": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py
index 9bdc918be9c..4fc66d0ee70 100644
--- a/homeassistant/components/flume/__init__.py
+++ b/homeassistant/components/flume/__init__.py
@@ -53,7 +53,7 @@ def _setup_entry(hass: HomeAssistant, entry: ConfigEntry):
return flume_auth, flume_devices, http_session
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up flume from a config entry."""
flume_auth, flume_devices, http_session = await hass.async_add_executor_job(
diff --git a/homeassistant/components/flume/translations/he.json b/homeassistant/components/flume/translations/he.json
index ac90b3264ea..0dec935b9a2 100644
--- a/homeassistant/components/flume/translations/he.json
+++ b/homeassistant/components/flume/translations/he.json
@@ -1,6 +1,21 @@
{
"config": {
+ "abort": {
+ "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",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
+ "reauth_confirm": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 {username} \u05d0\u05d9\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea \u05e2\u05d5\u05d3."
+ },
"user": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
diff --git a/homeassistant/components/flunearyou/translations/he.json b/homeassistant/components/flunearyou/translations/he.json
index 4c49313d977..02a79d5fbcc 100644
--- a/homeassistant/components/flunearyou/translations/he.json
+++ b/homeassistant/components/flunearyou/translations/he.json
@@ -1,8 +1,15 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
"user": {
"data": {
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
"longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
}
}
diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json
index 9f89045b28e..709e95f476b 100644
--- a/homeassistant/components/folder_watcher/manifest.json
+++ b/homeassistant/components/folder_watcher/manifest.json
@@ -2,7 +2,7 @@
"domain": "folder_watcher",
"name": "Folder Watcher",
"documentation": "https://www.home-assistant.io/integrations/folder_watcher",
- "requirements": ["watchdog==2.1.2"],
+ "requirements": ["watchdog==2.1.3"],
"codeowners": [],
"quality_scale": "internal",
"iot_class": "local_polling"
diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py
new file mode 100644
index 00000000000..b00e5f1c4ce
--- /dev/null
+++ b/homeassistant/components/forecast_solar/__init__.py
@@ -0,0 +1,79 @@
+"""The Forecast.Solar integration."""
+from __future__ import annotations
+
+from datetime import timedelta
+import logging
+
+from forecast_solar import ForecastSolar
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import (
+ CONF_AZIMUTH,
+ CONF_DAMPING,
+ CONF_DECLINATION,
+ CONF_MODULES_POWER,
+ DOMAIN,
+)
+
+PLATFORMS = ["sensor"]
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up Forecast.Solar from a config entry."""
+ api_key = entry.options.get(CONF_API_KEY)
+ # Our option flow may cause it to be an empty string,
+ # this if statement is here to catch that.
+ if not api_key:
+ api_key = None
+
+ forecast = ForecastSolar(
+ api_key=api_key,
+ latitude=entry.data[CONF_LATITUDE],
+ longitude=entry.data[CONF_LONGITUDE],
+ declination=entry.options[CONF_DECLINATION],
+ azimuth=(entry.options[CONF_AZIMUTH] - 180),
+ kwp=(entry.options[CONF_MODULES_POWER] / 1000),
+ damping=entry.options.get(CONF_DAMPING, 0),
+ )
+
+ # Free account have a resolution of 1 hour, using that as the default
+ # update interval. Using a higher value for accounts with an API key.
+ update_interval = timedelta(hours=1)
+ if api_key is not None:
+ update_interval = timedelta(minutes=30)
+
+ coordinator: DataUpdateCoordinator = DataUpdateCoordinator(
+ hass,
+ logging.getLogger(__name__),
+ name=DOMAIN,
+ update_method=forecast.estimate,
+ update_interval=update_interval,
+ )
+ await coordinator.async_config_entry_first_refresh()
+
+ hass.data.setdefault(DOMAIN, {})
+ hass.data[DOMAIN][entry.entry_id] = coordinator
+
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+
+ entry.async_on_unload(entry.add_update_listener(async_update_options))
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
+ """Update options."""
+ await hass.config_entries.async_reload(entry.entry_id)
diff --git a/homeassistant/components/forecast_solar/config_flow.py b/homeassistant/components/forecast_solar/config_flow.py
new file mode 100644
index 00000000000..256534da67a
--- /dev/null
+++ b/homeassistant/components/forecast_solar/config_flow.py
@@ -0,0 +1,119 @@
+"""Config flow for Forecast.Solar integration."""
+from __future__ import annotations
+
+from typing import Any
+
+import voluptuous as vol
+
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
+from homeassistant.const import CONF_API_KEY, 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 (
+ CONF_AZIMUTH,
+ CONF_DAMPING,
+ CONF_DECLINATION,
+ CONF_MODULES_POWER,
+ DOMAIN,
+)
+
+
+class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Forecast.Solar."""
+
+ VERSION = 1
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(
+ config_entry: ConfigEntry,
+ ) -> ForecastSolarOptionFlowHandler:
+ """Get the options flow for this handler."""
+ return ForecastSolarOptionFlowHandler(config_entry)
+
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Handle a flow initiated by the user."""
+ if user_input is not None:
+ return self.async_create_entry(
+ title=user_input[CONF_NAME],
+ data={
+ CONF_LATITUDE: user_input[CONF_LATITUDE],
+ CONF_LONGITUDE: user_input[CONF_LONGITUDE],
+ },
+ options={
+ CONF_AZIMUTH: user_input[CONF_AZIMUTH],
+ CONF_DECLINATION: user_input[CONF_DECLINATION],
+ CONF_MODULES_POWER: user_input[CONF_MODULES_POWER],
+ },
+ )
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_NAME, default=self.hass.config.location_name
+ ): str,
+ vol.Required(
+ CONF_LATITUDE, default=self.hass.config.latitude
+ ): cv.latitude,
+ vol.Required(
+ CONF_LONGITUDE, default=self.hass.config.longitude
+ ): cv.longitude,
+ vol.Required(CONF_DECLINATION, default=25): vol.All(
+ vol.Coerce(int), vol.Range(min=0, max=90)
+ ),
+ vol.Required(CONF_AZIMUTH, default=180): vol.All(
+ vol.Coerce(int), vol.Range(min=0, max=360)
+ ),
+ vol.Required(CONF_MODULES_POWER): vol.Coerce(int),
+ }
+ ),
+ )
+
+
+class ForecastSolarOptionFlowHandler(OptionsFlow):
+ """Handle options."""
+
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Manage the options."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ return self.async_show_form(
+ step_id="init",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_API_KEY,
+ default=self.config_entry.options.get(CONF_API_KEY, ""),
+ ): str,
+ vol.Required(
+ CONF_DECLINATION,
+ default=self.config_entry.options[CONF_DECLINATION],
+ ): vol.All(vol.Coerce(int), vol.Range(min=0, max=90)),
+ vol.Required(
+ CONF_AZIMUTH,
+ default=self.config_entry.options.get(CONF_AZIMUTH),
+ ): vol.All(vol.Coerce(int), vol.Range(min=-0, max=360)),
+ vol.Required(
+ CONF_MODULES_POWER,
+ default=self.config_entry.options[CONF_MODULES_POWER],
+ ): vol.Coerce(int),
+ vol.Optional(
+ CONF_DAMPING,
+ default=self.config_entry.options.get(CONF_DAMPING, 0.0),
+ ): vol.Coerce(float),
+ }
+ ),
+ )
diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py
new file mode 100644
index 00000000000..12aa1ee5362
--- /dev/null
+++ b/homeassistant/components/forecast_solar/const.py
@@ -0,0 +1,89 @@
+"""Constants for the Forecast.Solar integration."""
+from __future__ import annotations
+
+from typing import Final
+
+from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
+from homeassistant.const import (
+ DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_POWER,
+ DEVICE_CLASS_TIMESTAMP,
+ ENERGY_KILO_WATT_HOUR,
+ POWER_WATT,
+)
+
+from .models import ForecastSolarSensor
+
+DOMAIN = "forecast_solar"
+
+CONF_DECLINATION = "declination"
+CONF_AZIMUTH = "azimuth"
+CONF_MODULES_POWER = "modules power"
+CONF_DAMPING = "damping"
+ATTR_ENTRY_TYPE: Final = "entry_type"
+ENTRY_TYPE_SERVICE: Final = "service"
+
+SENSORS: list[ForecastSolarSensor] = [
+ ForecastSolarSensor(
+ key="energy_production_today",
+ name="Estimated Energy Production - Today",
+ device_class=DEVICE_CLASS_ENERGY,
+ unit_of_measurement=ENERGY_KILO_WATT_HOUR,
+ ),
+ ForecastSolarSensor(
+ key="energy_production_tomorrow",
+ name="Estimated Energy Production - Tomorrow",
+ device_class=DEVICE_CLASS_ENERGY,
+ unit_of_measurement=ENERGY_KILO_WATT_HOUR,
+ ),
+ ForecastSolarSensor(
+ key="power_highest_peak_time_today",
+ name="Highest Power Peak Time - Today",
+ device_class=DEVICE_CLASS_TIMESTAMP,
+ ),
+ ForecastSolarSensor(
+ key="power_highest_peak_time_tomorrow",
+ name="Highest Power Peak Time - Tomorrow",
+ device_class=DEVICE_CLASS_TIMESTAMP,
+ ),
+ ForecastSolarSensor(
+ key="power_production_now",
+ name="Estimated Power Production - Now",
+ device_class=DEVICE_CLASS_POWER,
+ state_class=STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=POWER_WATT,
+ ),
+ ForecastSolarSensor(
+ key="power_production_next_hour",
+ name="Estimated Power Production - Next Hour",
+ device_class=DEVICE_CLASS_POWER,
+ entity_registry_enabled_default=False,
+ unit_of_measurement=POWER_WATT,
+ ),
+ ForecastSolarSensor(
+ key="power_production_next_12hours",
+ name="Estimated Power Production - Next 12 Hours",
+ device_class=DEVICE_CLASS_POWER,
+ entity_registry_enabled_default=False,
+ unit_of_measurement=POWER_WATT,
+ ),
+ ForecastSolarSensor(
+ key="power_production_next_24hours",
+ name="Estimated Power Production - Next 24 Hours",
+ device_class=DEVICE_CLASS_POWER,
+ entity_registry_enabled_default=False,
+ unit_of_measurement=POWER_WATT,
+ ),
+ ForecastSolarSensor(
+ key="energy_current_hour",
+ name="Estimated Energy Production - This Hour",
+ device_class=DEVICE_CLASS_ENERGY,
+ unit_of_measurement=ENERGY_KILO_WATT_HOUR,
+ ),
+ ForecastSolarSensor(
+ key="energy_next_hour",
+ name="Estimated Energy Production - Next Hour",
+ device_class=DEVICE_CLASS_ENERGY,
+ unit_of_measurement=ENERGY_KILO_WATT_HOUR,
+ ),
+]
diff --git a/homeassistant/components/forecast_solar/manifest.json b/homeassistant/components/forecast_solar/manifest.json
new file mode 100644
index 00000000000..c17e8bd51f8
--- /dev/null
+++ b/homeassistant/components/forecast_solar/manifest.json
@@ -0,0 +1,10 @@
+{
+ "domain": "forecast_solar",
+ "name": "Forecast.Solar",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/forecast_solar",
+ "requirements": ["forecast_solar==1.3.1"],
+ "codeowners": ["@klaasnicolaas", "@frenck"],
+ "quality_scale": "platinum",
+ "iot_class": "cloud_polling"
+}
diff --git a/homeassistant/components/forecast_solar/models.py b/homeassistant/components/forecast_solar/models.py
new file mode 100644
index 00000000000..d01f17fc975
--- /dev/null
+++ b/homeassistant/components/forecast_solar/models.py
@@ -0,0 +1,17 @@
+"""Models for the Forecast.Solar integration."""
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+
+@dataclass
+class ForecastSolarSensor:
+ """Represents an Forecast.Solar Sensor."""
+
+ key: str
+ name: str
+
+ device_class: str | None = None
+ entity_registry_enabled_default: bool = True
+ state_class: str | None = None
+ unit_of_measurement: str | None = None
diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py
new file mode 100644
index 00000000000..a6b1927926e
--- /dev/null
+++ b/homeassistant/components/forecast_solar/sensor.py
@@ -0,0 +1,68 @@
+"""Support for the Forecast.Solar sensor service."""
+from __future__ import annotations
+
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_NAME
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import StateType
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
+
+from .const import ATTR_ENTRY_TYPE, DOMAIN, ENTRY_TYPE_SERVICE, SENSORS
+from .models import ForecastSolarSensor
+
+
+async def async_setup_entry(
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
+ """Defer sensor setup to the shared sensor module."""
+ coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+
+ async_add_entities(
+ ForecastSolarSensorEntity(
+ entry_id=entry.entry_id, coordinator=coordinator, sensor=sensor
+ )
+ for sensor in SENSORS
+ )
+
+
+class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity):
+ """Defines a Forcast.Solar sensor."""
+
+ def __init__(
+ self,
+ *,
+ entry_id: str,
+ coordinator: DataUpdateCoordinator,
+ sensor: ForecastSolarSensor,
+ ) -> None:
+ """Initialize Forcast.Solar sensor."""
+ super().__init__(coordinator=coordinator)
+ self._sensor = sensor
+
+ self.entity_id = f"{SENSOR_DOMAIN}.{sensor.key}"
+ self._attr_device_class = sensor.device_class
+ self._attr_entity_registry_enabled_default = (
+ sensor.entity_registry_enabled_default
+ )
+ self._attr_name = sensor.name
+ self._attr_state_class = sensor.state_class
+ self._attr_unique_id = f"{entry_id}_{sensor.key}"
+ self._attr_unit_of_measurement = sensor.unit_of_measurement
+
+ self._attr_device_info = {
+ ATTR_IDENTIFIERS: {(DOMAIN, entry_id)},
+ ATTR_NAME: "Solar Production Forecast",
+ ATTR_MANUFACTURER: "Forecast.Solar",
+ ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE,
+ }
+
+ @property
+ def state(self) -> StateType:
+ """Return the state of the sensor."""
+ state: StateType = getattr(self.coordinator.data, self._sensor.key)
+ return state
diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json
new file mode 100644
index 00000000000..e1ae451a04f
--- /dev/null
+++ b/homeassistant/components/forecast_solar/strings.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "description": "Fill in the data of your solar panels. Please refer to the documentation if a field is unclear.",
+ "data": {
+ "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)",
+ "declination": "Declination (0 = Horizontal, 90 = Vertical)",
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]",
+ "modules power": "Total Watt peak power of your solar modules",
+ "name": "[%key:common::config_flow::data::name%]"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation if a field is unclear.",
+ "data": {
+ "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",
+ "declination": "Declination (0 = Horizontal, 90 = Vertical)",
+ "modules power": "Total Watt peak power of your solar modules"
+ }
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/forecast_solar/translations/de.json b/homeassistant/components/forecast_solar/translations/de.json
new file mode 100644
index 00000000000..86b51a14845
--- /dev/null
+++ b/homeassistant/components/forecast_solar/translations/de.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "azimuth": "Azimut (360 Grad, 0 = Norden, 90 = Osten, 180 = S\u00fcden, 270 = Westen)",
+ "declination": "Deklination (0 = Horizontal, 90 = Vertikal)",
+ "latitude": "Breitengrad",
+ "longitude": "L\u00e4ngengrad",
+ "modules power": "Gesamt-Watt-Spitzenleistung Ihrer Solarmodule",
+ "name": "Name"
+ },
+ "description": "Gib die Daten deiner Solarmodule ein. Wenn ein Feld unklar ist, schlage bitte in der Dokumentation nach."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "api_key": "Forecast.Solar API-Schl\u00fcssel (optional)",
+ "azimuth": "Azimut (360 Grad, 0 = Norden, 90 = Osten, 180 = S\u00fcden, 270 = Westen)",
+ "damping": "D\u00e4mpfungsfaktor: passt die Ergebnisse morgens und abends an",
+ "declination": "Deklination (0 = Horizontal, 90 = Vertikal)",
+ "modules power": "Gesamt-Watt-Spitzenleistung Ihrer Solarmodule"
+ },
+ "description": "Mit diesen Werten kann das Solar.Forecast-Ergebnis angepasst werden. Wenn ein Feld unklar ist, lies bitte in der Dokumentation nach."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forecast_solar/translations/en.json b/homeassistant/components/forecast_solar/translations/en.json
new file mode 100644
index 00000000000..6de9cddc567
--- /dev/null
+++ b/homeassistant/components/forecast_solar/translations/en.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)",
+ "declination": "Declination (0 = Horizontal, 90 = Vertical)",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "modules power": "Total Watt peak power of your solar modules",
+ "name": "Name"
+ },
+ "description": "Fill in the data of your solar panels. Please refer to the documentation if a field is unclear."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "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",
+ "declination": "Declination (0 = Horizontal, 90 = Vertical)",
+ "modules power": "Total Watt peak power of your solar modules"
+ },
+ "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation is a field is unclear."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forecast_solar/translations/et.json b/homeassistant/components/forecast_solar/translations/et.json
new file mode 100644
index 00000000000..6dddf4a7496
--- /dev/null
+++ b/homeassistant/components/forecast_solar/translations/et.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "azimuth": "Asimuut (360 kraadi, 0 = p\u00f5hi, 90 = ida, 180 = l\u00f5una, 270 = l\u00e4\u00e4s)",
+ "declination": "Deklinatsioon (0 = horisontaalne, 90 = vertikaalne)",
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad",
+ "modules power": "P\u00e4ikesemoodulite koguv\u00f5imsus vattides",
+ "name": "Nimi"
+ },
+ "description": "Sisesta oma p\u00e4ikesepaneelide andmed. Kui v\u00e4li on ebaselge, loe dokumentatsiooni."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "api_key": "Forecast.Solar API v\u00f5ti (valikuline)",
+ "azimuth": "Asimuut (360 kraadi, 0 = p\u00f5hi, 90 = ida, 180 = l\u00f5una, 270 = l\u00e4\u00e4s)",
+ "damping": "Summutustegur: reguleerib tulemusi hommikul ja \u00f5htul",
+ "declination": "Deklinatsioon (0 = horisontaalne, 90 = vertikaalne)",
+ "modules power": "P\u00e4ikesemoodulite koguv\u00f5imsus vattides"
+ },
+ "description": "Need v\u00e4\u00e4rtused v\u00f5imaldavad muuta Solar.Forecast tulemust. Vaata dokumentatsiooni kui on ebaselge."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forecast_solar/translations/hu.json b/homeassistant/components/forecast_solar/translations/hu.json
new file mode 100644
index 00000000000..b863d10e907
--- /dev/null
+++ b/homeassistant/components/forecast_solar/translations/hu.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "N\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forecast_solar/translations/no.json b/homeassistant/components/forecast_solar/translations/no.json
new file mode 100644
index 00000000000..5ee0691ecda
--- /dev/null
+++ b/homeassistant/components/forecast_solar/translations/no.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "azimuth": "Azimut (360 grader, 0 = Nord, 90 = \u00d8st, 180 = S\u00f8r, 270 = Vest)",
+ "declination": "Deklinasjon (0 = horisontal, 90 = vertikal)",
+ "latitude": "Breddegrad",
+ "longitude": "Lengdegrad",
+ "modules power": "Total Watt-toppeffekt i solcellemodulene dine",
+ "name": "Navn"
+ },
+ "description": "Fyll ut dataene til solcellepanelene. Se dokumentasjonen hvis et felt er uklart."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "api_key": "Forecast.Solar API-n\u00f8kkel (valgfritt)",
+ "azimuth": "Azimut (360 grader, 0 = Nord, 90 = \u00d8st, 180 = S\u00f8r, 270 = Vest)",
+ "damping": "Dempingsfaktor: justerer resultatene om morgenen og kvelden",
+ "declination": "Deklinasjon (0 = horisontal, 90 = vertikal)",
+ "modules power": "Total Watt-toppeffekt i solcellemodulene dine"
+ },
+ "description": "Disse verdiene tillater justering av Solar.Forecast-resultatet. Se dokumentasjonen er et felt som er uklart."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forecast_solar/translations/ru.json b/homeassistant/components/forecast_solar/translations/ru.json
new file mode 100644
index 00000000000..1becfb2f5cb
--- /dev/null
+++ b/homeassistant/components/forecast_solar/translations/ru.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "azimuth": "\u0410\u0437\u0438\u043c\u0443\u0442 (360 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432, 0 = \u0441\u0435\u0432\u0435\u0440, 90 = \u0432\u043e\u0441\u0442\u043e\u043a, 180 = \u044e\u0433, 270 = \u0437\u0430\u043f\u0430\u0434)",
+ "declination": "\u0421\u043a\u043b\u043e\u043d\u0435\u043d\u0438\u0435 (0 = \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435, 90 = \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435)",
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430",
+ "modules power": "\u041e\u0431\u0449\u0430\u044f \u043f\u0438\u043a\u043e\u0432\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u0412\u0430\u0448\u0438\u0445 \u0441\u043e\u043b\u043d\u0435\u0447\u043d\u044b\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439 (\u0432 \u0412\u0430\u0442\u0442\u0430\u0445)",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435"
+ },
+ "description": "\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043e \u0412\u0430\u0448\u0438\u0445 \u0441\u043e\u043b\u043d\u0435\u0447\u043d\u044b\u0445 \u043f\u0430\u043d\u0435\u043b\u044f\u0445."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API Forecast.Solar (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)",
+ "azimuth": "\u0410\u0437\u0438\u043c\u0443\u0442 (360 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432, 0 = \u0441\u0435\u0432\u0435\u0440, 90 = \u0432\u043e\u0441\u0442\u043e\u043a, 180 = \u044e\u0433, 270 = \u0437\u0430\u043f\u0430\u0434)",
+ "damping": "\u0424\u0430\u043a\u0442\u043e\u0440 \u0437\u0430\u0442\u0443\u0445\u0430\u043d\u0438\u044f: \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u0438\u0440\u0443\u0435\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u0443\u0442\u0440\u043e\u043c \u0438 \u0432\u0435\u0447\u0435\u0440\u043e\u043c",
+ "declination": "\u0421\u043a\u043b\u043e\u043d\u0435\u043d\u0438\u0435 (0 = \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435, 90 = \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435)",
+ "modules power": "\u041e\u0431\u0449\u0430\u044f \u043f\u0438\u043a\u043e\u0432\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u0412\u0430\u0448\u0438\u0445 \u0441\u043e\u043b\u043d\u0435\u0447\u043d\u044b\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439 (\u0432 \u0412\u0430\u0442\u0442\u0430\u0445)"
+ },
+ "description": "\u042d\u0442\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0442 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0442\u044c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 Forecast.Solar."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forecast_solar/translations/zh-Hant.json b/homeassistant/components/forecast_solar/translations/zh-Hant.json
new file mode 100644
index 00000000000..43c7da0f593
--- /dev/null
+++ b/homeassistant/components/forecast_solar/translations/zh-Hant.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "azimuth": "\u65b9\u4f4d\u89d2\uff08360 \u5ea6\u55ae\u4f4d\u30020 = \u5317\u300190 = \u6771\u3001180 = \u5357\u3001270 = \u897f\uff09",
+ "declination": "\u504f\u89d2\uff080 = \u6c34\u5e73\u300190 = \u5782\u76f4\uff09",
+ "latitude": "\u7def\u5ea6",
+ "longitude": "\u7d93\u5ea6",
+ "modules power": "\u592a\u967d\u80fd\u6a21\u7d44\u7e3d\u5cf0\u503c\u529f\u7387",
+ "name": "\u540d\u7a31"
+ },
+ "description": "\u586b\u5beb\u592a\u967d\u80fd\u677f\u8cc7\u6599\u3002\u5982\u679c\u6709\u4e0d\u6e05\u695a\u7684\u5730\u65b9\uff0c\u8acb\u53c3\u8003\u6587\u4ef6\u8aaa\u660e\u3002"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "api_key": "Forecast.Solar API \u5bc6\u9470\uff08\u9078\u9805\uff09",
+ "azimuth": "\u65b9\u4f4d\u89d2\uff08360 \u5ea6\u55ae\u4f4d\u30020 = \u5317\u300190 = \u6771\u3001180 = \u5357\u3001270 = \u897f\uff09",
+ "damping": "\u963b\u5c3c\u56e0\u7d20\uff1a\u8abf\u6574\u6e05\u6668\u8207\u508d\u665a\u7d50\u679c",
+ "declination": "\u504f\u89d2\uff080 = \u6c34\u5e73\u300190 = \u5782\u76f4\uff09",
+ "modules power": "\u7e3d\u5cf0\u503c\u529f\u7387"
+ },
+ "description": "\u6b64\u4e9b\u6578\u503c\u5141\u8a31\u5fae\u8abf Solar.Forecast \u7d50\u679c\u3002\u5982\u679c\u6709\u4e0d\u6e05\u695a\u7684\u5730\u65b9\u3001\u8acb\u53c3\u8003\u6587\u4ef6\u8aaa\u660e\u3002"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json
index 0001157ce41..2047414b168 100644
--- a/homeassistant/components/forked_daapd/translations/de.json
+++ b/homeassistant/components/forked_daapd/translations/de.json
@@ -12,7 +12,7 @@
"wrong_password": "Ung\u00fcltiges Passwort",
"wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version > = 27.0 erforderlich."
},
- "flow_title": "Forked-Daapd-Server: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/forked_daapd/translations/he.json b/homeassistant/components/forked_daapd/translations/he.json
new file mode 100644
index 00000000000..31c52a3faad
--- /dev/null
+++ b/homeassistant/components/forked_daapd/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"
+ },
+ "error": {
+ "wrong_host_or_port": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8. \u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05d5\u05d0\u05ea \u05d4\u05d9\u05e6\u05d9\u05d0\u05d4.",
+ "wrong_password": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4."
+ },
+ "flow_title": "{name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05ea API (\u05d4\u05e9\u05d0\u05e8 \u05e8\u05d9\u05e7 \u05d0\u05dd \u05d0\u05d9\u05df \u05e1\u05d9\u05e1\u05de\u05d4)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json
index ca90fad3048..3400984dcd6 100644
--- a/homeassistant/components/forked_daapd/translations/hu.json
+++ b/homeassistant/components/forked_daapd/translations/hu.json
@@ -7,6 +7,7 @@
"forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.",
"unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py
index 2b2d14f60e0..1b9134bee44 100644
--- a/homeassistant/components/fortios/device_tracker.py
+++ b/homeassistant/components/fortios/device_tracker.py
@@ -46,29 +46,50 @@ def get_scanner(hass, config):
_LOGGER.error("Failed to login to FortiOS API: %s", ex)
return None
- return FortiOSDeviceScanner(fgt)
+ status_json = fgt.monitor("system/status", "")
+ fos_major_version = int(status_json["version"][1])
+
+ if fos_major_version < 6 or fos_major_version > 7:
+ _LOGGER.error(
+ "Unsupported FortiOS version, fos_major_version = %s",
+ fos_major_version,
+ )
+ return None
+
+ api_url = "user/device/query"
+ if fos_major_version == 6:
+ api_url = "user/device/select"
+
+ return FortiOSDeviceScanner(fgt, fos_major_version, api_url)
class FortiOSDeviceScanner(DeviceScanner):
"""This class queries a FortiOS unit for connected devices."""
- def __init__(self, fgt) -> None:
+ def __init__(self, fgt, fos_major_version, api_url) -> None:
"""Initialize the scanner."""
self._clients = {}
self._clients_json = {}
self._fgt = fgt
+ self._fos_major_version = fos_major_version
+ self._api_url = api_url
def update(self):
"""Update clients from the device."""
- clients_json = self._fgt.monitor("user/device/select", "")
+ clients_json = self._fgt.monitor(self._api_url, "")
self._clients_json = clients_json
self._clients = []
if clients_json:
- for client in clients_json["results"]:
- if client["last_seen"] < 180:
- self._clients.append(client["mac"].upper())
+ if self._fos_major_version == 6:
+ for client in clients_json["results"]:
+ if client["last_seen"] < 180:
+ self._clients.append(client["mac"].upper())
+ elif self._fos_major_version == 7:
+ for client in clients_json["results"]:
+ if client["is_online"]:
+ self._clients.append(client["mac"].upper())
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
@@ -90,7 +111,11 @@ class FortiOSDeviceScanner(DeviceScanner):
for client in data["results"]:
if client["mac"] == device:
try:
- name = client["host"]["name"]
+ name = ""
+ if self._fos_major_version == 6:
+ name = client["host"]["name"]
+ elif self._fos_major_version == 7:
+ name = client["hostname"]
_LOGGER.debug("Getting device name=%s", name)
return name
except KeyError as kex:
diff --git a/homeassistant/components/fortios/manifest.json b/homeassistant/components/fortios/manifest.json
index 251cb900adc..cc351441cdd 100644
--- a/homeassistant/components/fortios/manifest.json
+++ b/homeassistant/components/fortios/manifest.json
@@ -2,7 +2,7 @@
"domain": "fortios",
"name": "FortiOS",
"documentation": "https://www.home-assistant.io/integrations/fortios/",
- "requirements": ["fortiosapi==0.10.8"],
+ "requirements": ["fortiosapi==1.0.5"],
"codeowners": ["@kimfrellsen"],
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py
index 308b1a3cc9f..9d825ed0851 100644
--- a/homeassistant/components/foscam/__init__.py
+++ b/homeassistant/components/foscam/__init__.py
@@ -13,7 +13,7 @@ from .const import CONF_RTSP_PORT, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRES
PLATFORMS = ["camera"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up foscam from a config entry."""
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
@@ -36,26 +36,26 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
return unload_ok
-async def async_migrate_entry(hass, config_entry: ConfigEntry):
+async def async_migrate_entry(hass, entry: ConfigEntry):
"""Migrate old entry."""
- LOGGER.debug("Migrating from version %s", config_entry.version)
+ LOGGER.debug("Migrating from version %s", entry.version)
- if config_entry.version == 1:
+ if entry.version == 1:
# Change unique id
@callback
def update_unique_id(entry):
- return {"new_unique_id": config_entry.entry_id}
+ return {"new_unique_id": entry.entry_id}
- await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
+ await async_migrate_entries(hass, entry.entry_id, update_unique_id)
- config_entry.unique_id = None
+ entry.unique_id = None
# Get RTSP port from the camera or use the fallback one and store it in data
camera = FoscamCamera(
- config_entry.data[CONF_HOST],
- config_entry.data[CONF_PORT],
- config_entry.data[CONF_USERNAME],
- config_entry.data[CONF_PASSWORD],
+ entry.data[CONF_HOST],
+ entry.data[CONF_PORT],
+ entry.data[CONF_USERNAME],
+ entry.data[CONF_PASSWORD],
verbose=False,
)
@@ -66,11 +66,11 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry):
if ret != 0:
rtsp_port = response.get("rtspPort") or response.get("mediaPort")
- config_entry.data = {**config_entry.data, CONF_RTSP_PORT: rtsp_port}
+ entry.data = {**entry.data, CONF_RTSP_PORT: rtsp_port}
# Change entry version
- config_entry.version = 2
+ entry.version = 2
- LOGGER.info("Migration to version %s successful", config_entry.version)
+ LOGGER.info("Migration to version %s successful", entry.version)
return True
diff --git a/homeassistant/components/foscam/translations/de.json b/homeassistant/components/foscam/translations/de.json
index d87044b579a..bc1c12ea130 100644
--- a/homeassistant/components/foscam/translations/de.json
+++ b/homeassistant/components/foscam/translations/de.json
@@ -16,6 +16,7 @@
"password": "Passwort",
"port": "Port",
"rtsp_port": "RTSP-Port",
+ "stream": "Stream",
"username": "Benutzername"
}
}
diff --git a/homeassistant/components/foscam/translations/he.json b/homeassistant/components/foscam/translations/he.json
new file mode 100644
index 00000000000..4f3eeb63e8c
--- /dev/null
+++ b/homeassistant/components/foscam/translations/he.json
@@ -0,0 +1,22 @@
+{
+ "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",
+ "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": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py
index 44816a5c8ae..bb308e154ef 100644
--- a/homeassistant/components/freebox/__init__.py
+++ b/homeassistant/components/freebox/__init__.py
@@ -39,7 +39,7 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Freebox entry."""
router = FreeboxRouter(hass, entry)
await router.setup()
diff --git a/homeassistant/components/freebox/translations/he.json b/homeassistant/components/freebox/translations/he.json
new file mode 100644
index 00000000000..58521f503e2
--- /dev/null
+++ b/homeassistant/components/freebox/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py
new file mode 100644
index 00000000000..47c0bda1b1c
--- /dev/null
+++ b/homeassistant/components/freedompro/__init__.py
@@ -0,0 +1,84 @@
+"""Support for freedompro."""
+from datetime import timedelta
+import logging
+
+from pyfreedompro import get_list, get_states
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_API_KEY
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = ["light"]
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Freedompro from a config entry."""
+ hass.data.setdefault(DOMAIN, {})
+ api_key = entry.data[CONF_API_KEY]
+
+ coordinator = FreedomproDataUpdateCoordinator(hass, api_key)
+ await coordinator.async_config_entry_first_refresh()
+
+ entry.async_on_unload(entry.add_update_listener(update_listener))
+
+ hass.data[DOMAIN][entry.entry_id] = coordinator
+
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+async def update_listener(hass, config_entry):
+ """Update listener."""
+ await hass.config_entries.async_reload(config_entry.entry_id)
+
+
+class FreedomproDataUpdateCoordinator(DataUpdateCoordinator):
+ """Class to manage fetching Freedompro data API."""
+
+ def __init__(self, hass, api_key):
+ """Initialize."""
+ self._hass = hass
+ self._api_key = api_key
+ self._devices = None
+
+ update_interval = timedelta(minutes=1)
+ super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
+
+ async def _async_update_data(self):
+ if self._devices is None:
+ result = await get_list(
+ aiohttp_client.async_get_clientsession(self._hass), self._api_key
+ )
+ if result["state"]:
+ self._devices = result["devices"]
+ else:
+ raise UpdateFailed()
+
+ result = await get_states(
+ aiohttp_client.async_get_clientsession(self._hass), self._api_key
+ )
+
+ for device in self._devices:
+ dev = next(
+ (dev for dev in result if dev["uid"] == device["uid"]),
+ None,
+ )
+ if dev is not None and "state" in dev:
+ device["state"] = dev["state"]
+ return self._devices
diff --git a/homeassistant/components/freedompro/config_flow.py b/homeassistant/components/freedompro/config_flow.py
new file mode 100644
index 00000000000..c1288e61406
--- /dev/null
+++ b/homeassistant/components/freedompro/config_flow.py
@@ -0,0 +1,73 @@
+"""Config flow to configure Freedompro."""
+from pyfreedompro import get_list
+import voluptuous as vol
+
+from homeassistant import config_entries, core, exceptions
+from homeassistant.const import CONF_API_KEY
+from homeassistant.helpers import aiohttp_client
+
+from .const import DOMAIN
+
+STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str})
+
+
+class Hub:
+ """Freedompro Hub class."""
+
+ def __init__(self, hass, api_key):
+ """Freedompro Hub class init."""
+ self._hass = hass
+ self._api_key = api_key
+
+ async def authenticate(self):
+ """Freedompro Hub class authenticate."""
+ return await get_list(
+ aiohttp_client.async_get_clientsession(self._hass), self._api_key
+ )
+
+
+async def validate_input(hass: core.HomeAssistant, api_key):
+ """Validate api key."""
+ hub = Hub(hass, api_key)
+ result = await hub.authenticate()
+ if result["state"] is False:
+ if result["code"] == -201:
+ raise InvalidAuth
+ if result["code"] == -200:
+ raise CannotConnect
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow."""
+
+ VERSION = 1
+
+ async def async_step_user(self, user_input=None):
+ """Show the setup form to the user."""
+ if user_input is None:
+ return self.async_show_form(
+ step_id="user", data_schema=STEP_USER_DATA_SCHEMA
+ )
+
+ errors = {}
+
+ try:
+ await validate_input(self.hass, user_input[CONF_API_KEY])
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+ except InvalidAuth:
+ errors["base"] = "invalid_auth"
+ else:
+ return self.async_create_entry(title="Freedompro", data=user_input)
+
+ return self.async_show_form(
+ step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
+ )
+
+
+class CannotConnect(exceptions.HomeAssistantError):
+ """Error to indicate we cannot connect."""
+
+
+class InvalidAuth(exceptions.HomeAssistantError):
+ """Error to indicate there is invalid auth."""
diff --git a/homeassistant/components/freedompro/const.py b/homeassistant/components/freedompro/const.py
new file mode 100644
index 00000000000..3f5df9283d4
--- /dev/null
+++ b/homeassistant/components/freedompro/const.py
@@ -0,0 +1,3 @@
+"""Constants for the Freedompro integration."""
+
+DOMAIN = "freedompro"
diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py
new file mode 100644
index 00000000000..ca96dba00f7
--- /dev/null
+++ b/homeassistant/components/freedompro/light.py
@@ -0,0 +1,109 @@
+"""Support for Freedompro light."""
+import json
+
+from pyfreedompro import put_state
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_HS_COLOR,
+ COLOR_MODE_BRIGHTNESS,
+ COLOR_MODE_HS,
+ COLOR_MODE_ONOFF,
+ LightEntity,
+)
+from homeassistant.const import CONF_API_KEY
+from homeassistant.core import callback
+from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Freedompro light."""
+ api_key = entry.data[CONF_API_KEY]
+ coordinator = hass.data[DOMAIN][entry.entry_id]
+ async_add_entities(
+ Device(hass, api_key, device, coordinator)
+ for device in coordinator.data
+ if device["type"] == "lightbulb"
+ )
+
+
+class Device(CoordinatorEntity, LightEntity):
+ """Representation of an Freedompro light."""
+
+ def __init__(self, hass, api_key, device, coordinator):
+ """Initialize the Freedompro light."""
+ super().__init__(coordinator)
+ self._hass = hass
+ self._session = aiohttp_client.async_get_clientsession(self._hass)
+ self._api_key = api_key
+ self._attr_name = device["name"]
+ self._attr_unique_id = device["uid"]
+ self._type = device["type"]
+ self._characteristics = device["characteristics"]
+ self._attr_is_on = False
+ self._attr_brightness = 0
+ color_mode = COLOR_MODE_ONOFF
+ if "hue" in self._characteristics:
+ color_mode = COLOR_MODE_HS
+ elif "brightness" in self._characteristics:
+ color_mode = COLOR_MODE_BRIGHTNESS
+ self._attr_color_mode = color_mode
+ self._attr_supported_color_modes = {color_mode}
+
+ @callback
+ def _handle_coordinator_update(self) -> None:
+ """Handle updated data from the coordinator."""
+ device = next(
+ (
+ device
+ for device in self.coordinator.data
+ if device["uid"] == self._attr_unique_id
+ ),
+ None,
+ )
+ if device is not None and "state" in device:
+ state = device["state"]
+ if "on" in state:
+ self._attr_is_on = state["on"]
+ if "brightness" in state:
+ self._attr_brightness = round(state["brightness"] / 100 * 255)
+ if "hue" in state and "saturation" in state:
+ self._attr_hs_color = (state["hue"], state["saturation"])
+ 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()
+
+ async def async_turn_on(self, **kwargs):
+ """Async function to set on to light."""
+ payload = {"on": True}
+ if ATTR_BRIGHTNESS in kwargs:
+ payload["brightness"] = round(kwargs[ATTR_BRIGHTNESS] / 255 * 100)
+ if ATTR_HS_COLOR in kwargs:
+ payload["saturation"] = round(kwargs[ATTR_HS_COLOR][1])
+ payload["hue"] = round(kwargs[ATTR_HS_COLOR][0])
+ payload = json.dumps(payload)
+ await put_state(
+ self._session,
+ self._api_key,
+ self._attr_unique_id,
+ payload,
+ )
+ await self.coordinator.async_request_refresh()
+
+ async def async_turn_off(self, **kwargs):
+ """Async function to set off to light."""
+ payload = {"on": False}
+ payload = json.dumps(payload)
+ await put_state(
+ self._session,
+ self._api_key,
+ self._attr_unique_id,
+ payload,
+ )
+ await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/freedompro/manifest.json b/homeassistant/components/freedompro/manifest.json
new file mode 100644
index 00000000000..94d57b37cae
--- /dev/null
+++ b/homeassistant/components/freedompro/manifest.json
@@ -0,0 +1,11 @@
+{
+ "domain": "freedompro",
+ "name": "Freedompro",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/freedompro",
+ "codeowners": [
+ "@stefano055415"
+ ],
+ "requirements": ["pyfreedompro==1.1.0"],
+ "iot_class": "cloud_polling"
+}
diff --git a/homeassistant/components/freedompro/strings.json b/homeassistant/components/freedompro/strings.json
new file mode 100644
index 00000000000..947a9bd2e33
--- /dev/null
+++ b/homeassistant/components/freedompro/strings.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "[%key:common::config_flow::data::api_key%]"
+ },
+ "description": "Please enter the API key obtained from https://home.freedompro.eu",
+ "title": "Freedompro API key"
+ }
+ },
+ "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_device%]"
+ }
+ }
+}
diff --git a/homeassistant/components/freedompro/translations/en.json b/homeassistant/components/freedompro/translations/en.json
new file mode 100644
index 00000000000..c8952d56bfd
--- /dev/null
+++ b/homeassistant/components/freedompro/translations/en.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key"
+ },
+ "description": "Please enter the API key obtained from https://home.freedompro.eu",
+ "title": "Freedompro API key"
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py
index 35e924c807c..6d0030685b2 100644
--- a/homeassistant/components/fritz/__init__.py
+++ b/homeassistant/components/fritz/__init__.py
@@ -11,7 +11,7 @@ from homeassistant.const import (
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .common import FritzBoxTools, FritzData
@@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DATA_FRITZ] = FritzData()
@callback
- def _async_unload(event):
+ def _async_unload(event: Event) -> None:
fritz_tools.async_unload()
entry.async_on_unload(
@@ -83,7 +83,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok
-async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
+async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update when config_entry options update."""
if entry.options:
await hass.config_entries.async_reload(entry.entry_id)
diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py
index 65780fffaa9..7655df6e298 100644
--- a/homeassistant/components/fritz/binary_sensor.py
+++ b/homeassistant/components/fritz/binary_sensor.py
@@ -1,4 +1,4 @@
-"""AVM FRITZ!Box connectivitiy sensor."""
+"""AVM FRITZ!Box connectivity sensor."""
import logging
from fritzconnection.core.exceptions import FritzConnectionException
@@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .common import FritzBoxBaseEntity, FritzBoxTools
from .const import DOMAIN
@@ -17,13 +18,13 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
- hass: HomeAssistant, entry: ConfigEntry, async_add_entities
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up entry."""
_LOGGER.debug("Setting up FRITZ!Box binary sensors")
fritzbox_tools: FritzBoxTools = hass.data[DOMAIN][entry.entry_id]
- if "WANIPConn1" in fritzbox_tools.connection.services:
+ if fritzbox_tools.connection and "WANIPConn1" in fritzbox_tools.connection.services:
# Only routers are supported at the moment
async_add_entities(
[FritzBoxConnectivitySensor(fritzbox_tools, entry.title)], True
@@ -44,12 +45,12 @@ class FritzBoxConnectivitySensor(FritzBoxBaseEntity, BinarySensorEntity):
super().__init__(fritzbox_tools, device_friendly_name)
@property
- def name(self):
+ def name(self) -> str:
"""Return name."""
return self._name
@property
- def device_class(self):
+ def device_class(self) -> str:
"""Return device class."""
return DEVICE_CLASS_CONNECTIVITY
@@ -59,7 +60,7 @@ class FritzBoxConnectivitySensor(FritzBoxBaseEntity, BinarySensorEntity):
return self._is_on
@property
- def unique_id(self):
+ def unique_id(self) -> str:
"""Return unique id."""
return self._unique_id
@@ -73,14 +74,19 @@ class FritzBoxConnectivitySensor(FritzBoxBaseEntity, BinarySensorEntity):
_LOGGER.debug("Updating FRITZ!Box binary sensors")
self._is_on = True
try:
- if "WANCommonInterfaceConfig1" in self._fritzbox_tools.connection.services:
+ if (
+ self._fritzbox_tools.connection
+ and "WANCommonInterfaceConfig1"
+ in self._fritzbox_tools.connection.services
+ ):
link_props = self._fritzbox_tools.connection.call_action(
"WANCommonInterfaceConfig1", "GetCommonLinkProperties"
)
is_up = link_props["NewPhysicalLinkStatus"]
self._is_on = is_up == "Up"
else:
- self._is_on = self._fritzbox_tools.fritz_status.is_connected
+ if self._fritzbox_tools.fritz_status:
+ self._is_on = self._fritzbox_tools.fritz_status.is_connected
self._is_available = True
diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py
index 84288fe7fb3..776c7a7a22e 100644
--- a/homeassistant/components/fritz/common.py
+++ b/homeassistant/components/fritz/common.py
@@ -1,12 +1,12 @@
"""Support for AVM FRITZ!Box classes."""
from __future__ import annotations
-from dataclasses import dataclass
+from dataclasses import dataclass, field
from datetime import datetime, timedelta
import logging
-from typing import Any
+from types import MappingProxyType
+from typing import Any, Callable, TypedDict
-# pylint: disable=import-error
from fritzconnection import FritzConnection
from fritzconnection.core.exceptions import (
FritzActionError,
@@ -15,15 +15,17 @@ from fritzconnection.core.exceptions import (
)
from fritzconnection.lib.fritzhosts import FritzHosts
from fritzconnection.lib.fritzstatus import FritzStatus
+from fritzprofiles import FritzProfileSwitch, get_all_profiles
from homeassistant.components.device_tracker.const import (
CONF_CONSIDER_HOME,
DEFAULT_CONSIDER_HOME,
)
-from homeassistant.core import callback
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import dispatcher_send
+from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import dt as dt_util
@@ -40,6 +42,14 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
+class ClassSetupMissing(Exception):
+ """Raised when a Class func is called before setup."""
+
+ def __init__(self) -> None:
+ """Init custom exception."""
+ super().__init__("Function called before Class setup")
+
+
@dataclass
class Device:
"""FRITZ!Box device class."""
@@ -49,39 +59,49 @@ class Device:
name: str
+class HostInfo(TypedDict):
+ """FRITZ!Box host info class."""
+
+ mac: str
+ name: str
+ ip: str
+ status: bool
+
+
class FritzBoxTools:
"""FrtizBoxTools class."""
def __init__(
self,
- hass,
- password,
- username=DEFAULT_USERNAME,
- host=DEFAULT_HOST,
- port=DEFAULT_PORT,
- ):
+ hass: HomeAssistant,
+ password: str,
+ username: str = DEFAULT_USERNAME,
+ host: str = DEFAULT_HOST,
+ port: int = DEFAULT_PORT,
+ ) -> None:
"""Initialize FritzboxTools class."""
- self._cancel_scan = None
+ self._cancel_scan: CALLBACK_TYPE | None = None
self._devices: dict[str, Any] = {}
- self._options = None
- self._unique_id = None
- self.connection = None
- self.fritz_hosts = None
- self.fritz_status = None
+ self._options: MappingProxyType[str, Any] | None = None
+ self._unique_id: str | None = None
+ self.connection: FritzConnection = None
+ self.fritz_hosts: FritzHosts = None
+ self.fritz_profiles: dict[str, FritzProfileSwitch] = {}
+ self.fritz_status: FritzStatus = None
self.hass = hass
self.host = host
self.password = password
self.port = port
self.username = username
- self.mac = None
- self.model = None
- self.sw_version = None
+ self._mac: str | None = None
+ self._model: str | None = None
+ self._sw_version: str | None = None
- async def async_setup(self):
+ async def async_setup(self) -> None:
"""Wrap up FritzboxTools class setup."""
- return await self.hass.async_add_executor_job(self.setup)
+ await self.hass.async_add_executor_job(self.setup)
- def setup(self):
+ def setup(self) -> None:
"""Set up FritzboxTools class."""
self.connection = FritzConnection(
address=self.host,
@@ -93,14 +113,20 @@ class FritzBoxTools:
self.fritz_status = FritzStatus(fc=self.connection)
info = self.connection.call_action("DeviceInfo:1", "GetInfo")
- if self._unique_id is None:
+ if not self._unique_id:
self._unique_id = info["NewSerialNumber"]
- self.model = info.get("NewModelName")
- self.sw_version = info.get("NewSoftwareVersion")
- self.mac = self.unique_id
+ self._model = info.get("NewModelName")
+ self._sw_version = info.get("NewSoftwareVersion")
- async def async_start(self, options):
+ self.fritz_profiles = {
+ profile: FritzProfileSwitch(
+ "http://" + self.host, self.username, self.password, profile
+ )
+ for profile in get_all_profiles(self.host, self.username, self.password)
+ }
+
+ async def async_start(self, options: MappingProxyType[str, Any]) -> None:
"""Start FritzHosts connection."""
self.fritz_hosts = FritzHosts(fc=self.connection)
self._options = options
@@ -111,7 +137,7 @@ class FritzBoxTools:
)
@callback
- def async_unload(self):
+ def async_unload(self) -> None:
"""Unload FritzboxTools class."""
_LOGGER.debug("Unloading FRITZ!Box router integration")
if self._cancel_scan is not None:
@@ -119,8 +145,31 @@ class FritzBoxTools:
self._cancel_scan = None
@property
- def unique_id(self):
+ def unique_id(self) -> str:
"""Return unique id."""
+ if not self._unique_id:
+ raise ClassSetupMissing()
+ return self._unique_id
+
+ @property
+ def model(self) -> str:
+ """Return device model."""
+ if not self._model:
+ raise ClassSetupMissing()
+ return self._model
+
+ @property
+ def sw_version(self) -> str:
+ """Return SW version."""
+ if not self._sw_version:
+ raise ClassSetupMissing()
+ return self._sw_version
+
+ @property
+ def mac(self) -> str:
+ """Return device Mac address."""
+ if not self._unique_id:
+ raise ClassSetupMissing()
return self._unique_id
@property
@@ -138,7 +187,7 @@ class FritzBoxTools:
"""Event specific per FRITZ!Box entry to signal updates in devices."""
return f"{DOMAIN}-device-update-{self._unique_id}"
- def _update_info(self):
+ def _update_info(self) -> list[HostInfo]:
"""Retrieve latest information from the FRITZ!Box."""
return self.fritz_hosts.get_hosts_info()
@@ -146,9 +195,12 @@ class FritzBoxTools:
"""Scan for new devices and return a list of found device ids."""
_LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host)
- consider_home = self._options.get(
- CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
- )
+ if self._options:
+ consider_home = self._options.get(
+ CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
+ )
+ else:
+ consider_home = DEFAULT_CONSIDER_HOME
new_device = False
for known_host in self._update_info():
@@ -165,7 +217,7 @@ class FritzBoxTools:
if dev_mac in self._devices:
self._devices[dev_mac].update(dev_info, dev_home, consider_home)
else:
- device = FritzDevice(dev_mac)
+ device = FritzDevice(dev_mac, dev_name)
device.update(dev_info, dev_home, consider_home)
self._devices[dev_mac] = device
new_device = True
@@ -177,6 +229,10 @@ class FritzBoxTools:
async def service_fritzbox(self, service: str) -> None:
"""Define FRITZ!Box services."""
_LOGGER.debug("FRITZ!Box router: %s", service)
+
+ if not self.connection:
+ raise HomeAssistantError("Unable to establish a connection")
+
try:
if service == SERVICE_REBOOT:
await self.hass.async_add_executor_job(
@@ -194,26 +250,25 @@ class FritzBoxTools:
raise HomeAssistantError("Service not supported") from ex
+@dataclass
class FritzData:
"""Storage class for platform global data."""
- def __init__(self) -> None:
- """Initialize the data."""
- self.tracked: dict = {}
+ tracked: dict = field(default_factory=dict)
class FritzDevice:
"""FritzScanner device."""
- def __init__(self, mac, name=None):
+ def __init__(self, mac: str, name: str) -> None:
"""Initialize device info."""
self._mac = mac
self._name = name
- self._ip_address = None
- self._last_activity = None
+ self._ip_address: str | None = None
+ self._last_activity: datetime | None = None
self._connected = False
- def update(self, dev_info, dev_home, consider_home):
+ def update(self, dev_info: Device, dev_home: bool, consider_home: float) -> None:
"""Update device info."""
utc_point_in_time = dt_util.utcnow()
@@ -235,31 +290,42 @@ class FritzDevice:
self._ip_address = dev_info.ip_address if self._connected else None
@property
- def is_connected(self):
+ def is_connected(self) -> bool:
"""Return connected status."""
return self._connected
@property
- def mac_address(self):
+ def mac_address(self) -> str:
"""Get MAC address."""
return self._mac
@property
- def hostname(self):
+ def hostname(self) -> str:
"""Get Name."""
return self._name
@property
- def ip_address(self):
+ def ip_address(self) -> str | None:
"""Get IP address."""
return self._ip_address
@property
- def last_activity(self):
+ def last_activity(self) -> datetime | None:
"""Return device last activity."""
return self._last_activity
+class SwitchInfo(TypedDict):
+ """FRITZ!Box switch info class."""
+
+ description: str
+ friendly_name: str
+ icon: str
+ type: str
+ callback_update: Callable
+ callback_switch: Callable
+
+
class FritzBoxBaseEntity:
"""Fritz host entity base class."""
@@ -274,7 +340,7 @@ class FritzBoxBaseEntity:
return self._fritzbox_tools.mac
@property
- def device_info(self):
+ def device_info(self) -> DeviceInfo:
"""Return the device information."""
return {
diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py
index 4001dcadc71..5ca351cdec1 100644
--- a/homeassistant/components/fritz/config_flow.py
+++ b/homeassistant/components/fritz/config_flow.py
@@ -3,7 +3,7 @@ from __future__ import annotations
import logging
from typing import Any
-from urllib.parse import urlparse
+from urllib.parse import ParseResult, urlparse
from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError
import voluptuous as vol
@@ -21,6 +21,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
+from homeassistant.helpers.typing import DiscoveryInfoType
from .common import FritzBoxTools
from .const import (
@@ -42,23 +43,26 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(config_entry):
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
return FritzBoxToolsOptionsFlowHandler(config_entry)
- def __init__(self):
+ def __init__(self) -> None:
"""Initialize FRITZ!Box Tools flow."""
- self._host = None
- self._entry = None
- self._name = None
- self._password = None
- self._port = None
- self._username = None
- self.import_schema = None
- self.fritz_tools = None
+ self._host: str | None = None
+ self._entry: ConfigEntry
+ self._name: str
+ self._password: str
+ self._port: int | None = None
+ self._username: str
+ self.fritz_tools: FritzBoxTools
- async def fritz_tools_init(self):
+ async def fritz_tools_init(self) -> str | None:
"""Initialize FRITZ!Box Tools class."""
+
+ if not self._host or not self._port:
+ return None
+
self.fritz_tools = FritzBoxTools(
hass=self.hass,
host=self._host,
@@ -87,7 +91,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
return None
@callback
- def _async_create_entry(self):
+ def _async_create_entry(self) -> FlowResult:
"""Async create flow handler entry."""
return self.async_create_entry(
title=self._name,
@@ -102,12 +106,14 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
},
)
- async def async_step_ssdp(self, discovery_info):
+ async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult:
"""Handle a flow initialized by discovery."""
- ssdp_location = urlparse(discovery_info[ATTR_SSDP_LOCATION])
+ ssdp_location: ParseResult = urlparse(discovery_info[ATTR_SSDP_LOCATION])
self._host = ssdp_location.hostname
self._port = ssdp_location.port
- self._name = discovery_info.get(ATTR_UPNP_FRIENDLY_NAME)
+ self._name = (
+ discovery_info.get(ATTR_UPNP_FRIENDLY_NAME) or self.fritz_tools.model
+ )
self.context[CONF_HOST] = self._host
if uuid := discovery_info.get(ATTR_UPNP_UDN):
@@ -130,7 +136,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
}
return await self.async_step_confirm()
- async def async_step_confirm(self, user_input=None):
+ async def async_step_confirm(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
"""Handle user-confirmation of discovered node."""
if user_input is None:
return self._show_setup_form_confirm()
@@ -148,7 +156,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
return self._async_create_entry()
- def _show_setup_form_init(self, errors=None):
+ def _show_setup_form_init(self, errors: dict[str, str] | None = None) -> FlowResult:
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user",
@@ -163,7 +171,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors or {},
)
- def _show_setup_form_confirm(self, errors=None):
+ def _show_setup_form_confirm(
+ self, errors: dict[str, str] | None = None
+ ) -> FlowResult:
"""Show the setup form to the user."""
return self.async_show_form(
step_id="confirm",
@@ -177,7 +187,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors or {},
)
- async def async_step_user(self, user_input=None):
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
"""Handle a flow initiated by the user."""
if user_input is None:
return self._show_setup_form_init()
@@ -197,24 +209,28 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
return self._async_create_entry()
- async def async_step_reauth(self, data):
+ async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult:
"""Handle flow upon an API authentication error."""
- self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+ if cfg_entry := self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ ):
+ self._entry = cfg_entry
self._host = data[CONF_HOST]
self._port = data[CONF_PORT]
self._username = data[CONF_USERNAME]
self._password = data[CONF_PASSWORD]
return await self.async_step_reauth_confirm()
- def _show_setup_form_reauth_confirm(self, user_input, errors=None):
+ def _show_setup_form_reauth_confirm(
+ self, user_input: dict[str, Any], errors: dict[str, str] | None = None
+ ) -> FlowResult:
"""Show the reauth form to the user."""
+ default_username = user_input.get(CONF_USERNAME)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
- vol.Required(
- CONF_USERNAME, default=user_input.get(CONF_USERNAME)
- ): str,
+ vol.Required(CONF_USERNAME, default=default_username): str,
vol.Required(CONF_PASSWORD): str,
}
),
@@ -222,7 +238,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors or {},
)
- async def async_step_reauth_confirm(self, user_input=None):
+ async def async_step_reauth_confirm(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
"""Dialog that informs the user that reauth is required."""
if user_input is None:
return self._show_setup_form_reauth_confirm(
@@ -249,7 +267,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
await self.hass.config_entries.async_reload(self._entry.entry_id)
return self.async_abort(reason="reauth_successful")
- async def async_step_import(self, import_config):
+ async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
"""Import a config entry from configuration.yaml."""
return await self.async_step_user(
{
diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py
index 266e24b6be3..776c7a7dafa 100644
--- a/homeassistant/components/fritz/const.py
+++ b/homeassistant/components/fritz/const.py
@@ -2,7 +2,7 @@
DOMAIN = "fritz"
-PLATFORMS = ["binary_sensor", "device_tracker", "sensor"]
+PLATFORMS = ["binary_sensor", "device_tracker", "sensor", "switch"]
DATA_FRITZ = "fritz_data"
@@ -19,6 +19,14 @@ FRITZ_SERVICES = "fritz_services"
SERVICE_REBOOT = "reboot"
SERVICE_RECONNECT = "reconnect"
+SWITCH_PROFILE_STATUS_OFF = "never"
+SWITCH_PROFILE_STATUS_ON = "unlimited"
+
+SWITCH_TYPE_DEFLECTION = "CallDeflection"
+SWITCH_TYPE_DEVICEPROFILE = "DeviceProfile"
+SWITCH_TYPE_PORTFORWARD = "PortForward"
+SWITCH_TYPE_WIFINETWORK = "WiFiNetwork"
+
TRACKER_SCAN_INTERVAL = 30
UPTIME_DEVIATION = 5
diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py
index dbf6bc0df93..d4ff1dbd161 100644
--- a/homeassistant/components/fritz/device_tracker.py
+++ b/homeassistant/components/fritz/device_tracker.py
@@ -1,6 +1,7 @@
"""Support for FRITZ!Box routers."""
from __future__ import annotations
+import datetime
import logging
import voluptuous as vol
@@ -17,9 +18,11 @@ from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
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
-from .common import FritzBoxTools, FritzDevice
+from .common import Device, FritzBoxTools, FritzData, FritzDevice
from .const import DATA_FRITZ, DEFAULT_DEVICE_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -41,7 +44,7 @@ PLATFORM_SCHEMA = vol.All(
)
-async def async_get_scanner(hass: HomeAssistant, config: ConfigType):
+async def async_get_scanner(hass: HomeAssistant, config: ConfigType) -> None:
"""Import legacy FRITZ!Box configuration."""
_LOGGER.debug("Import legacy FRITZ!Box configuration from YAML")
@@ -63,15 +66,15 @@ async def async_get_scanner(hass: HomeAssistant, config: ConfigType):
async def async_setup_entry(
- hass: HomeAssistant, entry: ConfigEntry, async_add_entities
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up device tracker for FRITZ!Box component."""
_LOGGER.debug("Starting FRITZ!Box device tracker")
- router = hass.data[DOMAIN][entry.entry_id]
- data_fritz = hass.data[DATA_FRITZ]
+ router: FritzBoxTools = hass.data[DOMAIN][entry.entry_id]
+ data_fritz: FritzData = hass.data[DATA_FRITZ]
@callback
- def update_router():
+ def update_router() -> None:
"""Update the values of the router."""
_async_add_entities(router, async_add_entities, data_fritz)
@@ -83,10 +86,14 @@ async def async_setup_entry(
@callback
-def _async_add_entities(router, async_add_entities, data_fritz):
+def _async_add_entities(
+ router: FritzBoxTools,
+ async_add_entities: AddEntitiesCallback,
+ data_fritz: FritzData,
+) -> None:
"""Add new tracker entities from the router."""
- def _is_tracked(mac, device):
+ def _is_tracked(mac: str, device: Device) -> bool:
for tracked in data_fritz.tracked.values():
if mac in tracked:
return True
@@ -114,31 +121,32 @@ class FritzBoxTracker(ScannerEntity):
def __init__(self, router: FritzBoxTools, device: FritzDevice) -> None:
"""Initialize a FRITZ!Box device."""
self._router = router
- self._mac = device.mac_address
- self._name = device.hostname or DEFAULT_DEVICE_NAME
- self._last_activity = device.last_activity
+ self._mac: str = device.mac_address
+ self._name: str = device.hostname or DEFAULT_DEVICE_NAME
+ self._last_activity: datetime.datetime | None = device.last_activity
self._active = False
- self._attrs: dict = {}
@property
- def is_connected(self):
+ def is_connected(self) -> bool:
"""Return device status."""
return self._active
@property
- def name(self):
+ def name(self) -> str:
"""Return device name."""
return self._name
@property
- def unique_id(self):
+ def unique_id(self) -> str:
"""Return device unique id."""
return self._mac
@property
- def ip_address(self) -> str:
+ def ip_address(self) -> str | None:
"""Return the primary ip address of the device."""
- return self._router.devices[self._mac].ip_address
+ if self._mac:
+ return self._router.devices[self._mac].ip_address
+ return None
@property
def mac_address(self) -> str:
@@ -146,9 +154,11 @@ class FritzBoxTracker(ScannerEntity):
return self._mac
@property
- def hostname(self) -> str:
+ def hostname(self) -> str | None:
"""Return hostname of the device."""
- return self._router.devices[self._mac].hostname
+ if self._mac:
+ return self._router.devices[self._mac].hostname
+ return None
@property
def source_type(self) -> str:
@@ -156,7 +166,7 @@ class FritzBoxTracker(ScannerEntity):
return SOURCE_TYPE_ROUTER
@property
- def device_info(self):
+ def device_info(self) -> DeviceInfo:
"""Return the device information."""
return {
"connections": {(CONNECTION_NETWORK_MAC, self._mac)},
@@ -176,7 +186,7 @@ class FritzBoxTracker(ScannerEntity):
return False
@property
- def icon(self):
+ def icon(self) -> str:
"""Return device icon."""
if self.is_connected:
return "mdi:lan-connect"
@@ -200,17 +210,20 @@ class FritzBoxTracker(ScannerEntity):
@callback
def async_process_update(self) -> None:
"""Update device."""
- device: FritzDevice = self._router.devices[self._mac]
+ if not self._mac:
+ return
+
+ device = self._router.devices[self._mac]
self._active = device.is_connected
self._last_activity = device.last_activity
@callback
- def async_on_demand_update(self):
+ def async_on_demand_update(self) -> None:
"""Update state."""
self.async_process_update()
self.async_write_ha_state()
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Register state update callback."""
self.async_process_update()
self.async_on_remove(
diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json
index 1158ea2e797..d1c096a2ef5 100644
--- a/homeassistant/components/fritz/manifest.json
+++ b/homeassistant/components/fritz/manifest.json
@@ -3,8 +3,11 @@
"name": "AVM FRITZ!Box Tools",
"documentation": "https://www.home-assistant.io/integrations/fritz",
"requirements": [
- "fritzconnection==1.4.2"
+ "fritzconnection==1.4.2",
+ "fritzprofiles==0.6.1",
+ "xmltodict==0.12.0"
],
+ "dependencies": ["network"],
"codeowners": [
"@mammuth",
"@AaronDavidSchneider",
diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py
index 7bff6bd40c8..6d3a8f33c3c 100644
--- a/homeassistant/components/fritz/sensor.py
+++ b/homeassistant/components/fritz/sensor.py
@@ -74,7 +74,10 @@ async def async_setup_entry(
_LOGGER.debug("Setting up FRITZ!Box sensors")
fritzbox_tools: FritzBoxTools = hass.data[DOMAIN][entry.entry_id]
- if "WANIPConn1" not in fritzbox_tools.connection.services:
+ if (
+ not fritzbox_tools.connection
+ or "WANIPConn1" not in fritzbox_tools.connection.services
+ ):
# Only routers are supported at the moment
return
diff --git a/homeassistant/components/fritz/services.py b/homeassistant/components/fritz/services.py
index 7ed5ecd3c40..fcfbd54b743 100644
--- a/homeassistant/components/fritz/services.py
+++ b/homeassistant/components/fritz/services.py
@@ -10,14 +10,14 @@ from .const import DOMAIN, FRITZ_SERVICES, SERVICE_REBOOT, SERVICE_RECONNECT
_LOGGER = logging.getLogger(__name__)
-async def async_setup_services(hass: HomeAssistant):
+async def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services for Fritz integration."""
for service in [SERVICE_REBOOT, SERVICE_RECONNECT]:
if hass.services.has_service(DOMAIN, service):
return
- async def async_call_fritz_service(service_call):
+ async def async_call_fritz_service(service_call: ServiceCall) -> None:
"""Call correct Fritz service."""
if not (
@@ -40,10 +40,10 @@ async def async_setup_services(hass: HomeAssistant):
async def _async_get_configured_fritz_tools(
hass: HomeAssistant, service_call: ServiceCall
-):
+) -> list:
"""Get FritzBoxTools class from config entry."""
- list_entry_id = []
+ list_entry_id: list = []
for entry_id in await async_extract_config_entry_ids(hass, service_call):
config_entry = hass.config_entries.async_get_entry(entry_id)
if config_entry and config_entry.domain == DOMAIN:
@@ -51,7 +51,7 @@ async def _async_get_configured_fritz_tools(
return list_entry_id
-async def async_unload_services(hass: HomeAssistant):
+async def async_unload_services(hass: HomeAssistant) -> None:
"""Unload services for Fritz integration."""
if not hass.data.get(FRITZ_SERVICES):
diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py
new file mode 100644
index 00000000000..16eaecb178d
--- /dev/null
+++ b/homeassistant/components/fritz/switch.py
@@ -0,0 +1,642 @@
+"""Switches for AVM Fritz!Box functions."""
+from __future__ import annotations
+
+from collections import OrderedDict
+from functools import partial
+import logging
+from typing import Any
+
+from fritzconnection.core.exceptions import (
+ FritzActionError,
+ FritzActionFailedError,
+ FritzConnectionException,
+ FritzSecurityError,
+ FritzServiceError,
+)
+import xmltodict
+
+from homeassistant.components.switch import SwitchEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.util import get_local_ip, slugify
+
+from .common import FritzBoxBaseEntity, FritzBoxTools, SwitchInfo
+from .const import (
+ DOMAIN,
+ SWITCH_PROFILE_STATUS_OFF,
+ SWITCH_PROFILE_STATUS_ON,
+ SWITCH_TYPE_DEFLECTION,
+ SWITCH_TYPE_DEVICEPROFILE,
+ SWITCH_TYPE_PORTFORWARD,
+ SWITCH_TYPE_WIFINETWORK,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_service_call_action(
+ fritzbox_tools: FritzBoxTools,
+ service_name: str,
+ service_suffix: str | None,
+ action_name: str,
+ **kwargs: Any,
+) -> None | dict:
+ """Return service details."""
+ return await fritzbox_tools.hass.async_add_executor_job(
+ partial(
+ service_call_action,
+ fritzbox_tools,
+ service_name,
+ service_suffix,
+ action_name,
+ **kwargs,
+ )
+ )
+
+
+def service_call_action(
+ fritzbox_tools: FritzBoxTools,
+ service_name: str,
+ service_suffix: str | None,
+ action_name: str,
+ **kwargs: Any,
+) -> dict | None:
+ """Return service details."""
+
+ if f"{service_name}{service_suffix}" not in fritzbox_tools.connection.services:
+ return None
+
+ try:
+ return fritzbox_tools.connection.call_action(
+ f"{service_name}:{service_suffix}",
+ action_name,
+ **kwargs,
+ )
+ except FritzSecurityError:
+ _LOGGER.error(
+ "Authorization Error: Please check the provided credentials and verify that you can log into the web interface",
+ exc_info=True,
+ )
+ return None
+ except (FritzActionError, FritzActionFailedError, FritzServiceError):
+ _LOGGER.error(
+ "Service/Action Error: cannot execute service %s",
+ service_name,
+ exc_info=True,
+ )
+ return None
+ except FritzConnectionException:
+ _LOGGER.error(
+ "Connection Error: Please check the device is properly configured for remote login",
+ exc_info=True,
+ )
+ return None
+
+
+def get_deflections(
+ fritzbox_tools: FritzBoxTools, service_name: str
+) -> list[OrderedDict[Any, Any]] | None:
+ """Get deflection switch info."""
+
+ deflection_list = service_call_action(
+ fritzbox_tools,
+ service_name,
+ "1",
+ "GetDeflections",
+ )
+
+ if not deflection_list:
+ return []
+
+ items = xmltodict.parse(deflection_list["NewDeflectionList"])["List"]["Item"]
+ if not isinstance(items, list):
+ return [items]
+ return items
+
+
+def deflection_entities_list(
+ fritzbox_tools: FritzBoxTools, device_friendly_name: str
+) -> list[FritzBoxDeflectionSwitch]:
+ """Get list of deflection entities."""
+
+ _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_DEFLECTION)
+
+ service_name = "X_AVM-DE_OnTel"
+ deflections_response = service_call_action(
+ fritzbox_tools, service_name, "1", "GetNumberOfDeflections"
+ )
+ if not deflections_response:
+ _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
+ return []
+
+ _LOGGER.debug(
+ "Specific %s response: GetNumberOfDeflections=%s",
+ SWITCH_TYPE_DEFLECTION,
+ deflections_response,
+ )
+
+ if deflections_response["NewNumberOfDeflections"] == 0:
+ _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
+ return []
+
+ deflection_list = get_deflections(fritzbox_tools, service_name)
+ if deflection_list is None:
+ return []
+
+ return [
+ FritzBoxDeflectionSwitch(
+ fritzbox_tools, device_friendly_name, dict_of_deflection
+ )
+ for dict_of_deflection in deflection_list
+ ]
+
+
+def port_entities_list(
+ fritzbox_tools: FritzBoxTools, device_friendly_name: str
+) -> list[FritzBoxPortSwitch]:
+ """Get list of port forwarding entities."""
+
+ _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_PORTFORWARD)
+ entities_list: list = []
+ service_name = "Layer3Forwarding"
+ connection_type = service_call_action(
+ fritzbox_tools, service_name, "1", "GetDefaultConnectionService"
+ )
+ if not connection_type:
+ _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_PORTFORWARD)
+ return []
+
+ # Return NewDefaultConnectionService sample: "1.WANPPPConnection.1"
+ con_type: str = connection_type["NewDefaultConnectionService"][2:][:-2]
+
+ # Query port forwardings and setup a switch for each forward for the current device
+ resp = service_call_action(
+ fritzbox_tools, con_type, "1", "GetPortMappingNumberOfEntries"
+ )
+ if not resp:
+ _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
+ return []
+
+ port_forwards_count: int = resp["NewPortMappingNumberOfEntries"]
+
+ _LOGGER.debug(
+ "Specific %s response: GetPortMappingNumberOfEntries=%s",
+ SWITCH_TYPE_PORTFORWARD,
+ port_forwards_count,
+ )
+
+ local_ip = get_local_ip()
+ _LOGGER.debug("IP source for %s is %s", fritzbox_tools.host, local_ip)
+
+ for i in range(port_forwards_count):
+
+ portmap = service_call_action(
+ fritzbox_tools,
+ con_type,
+ "1",
+ "GetGenericPortMappingEntry",
+ NewPortMappingIndex=i,
+ )
+
+ if not portmap:
+ _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
+ continue
+
+ _LOGGER.debug(
+ "Specific %s response: GetGenericPortMappingEntry=%s",
+ SWITCH_TYPE_PORTFORWARD,
+ portmap,
+ )
+
+ # We can only handle port forwards of the given device
+ if portmap["NewInternalClient"] == local_ip:
+ entities_list.append(
+ FritzBoxPortSwitch(
+ fritzbox_tools,
+ device_friendly_name,
+ portmap,
+ i,
+ con_type,
+ )
+ )
+
+ return entities_list
+
+
+def profile_entities_list(
+ fritzbox_tools: FritzBoxTools, device_friendly_name: str
+) -> list[FritzBoxProfileSwitch]:
+ """Get list of profile entities."""
+ _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_DEVICEPROFILE)
+ if len(fritzbox_tools.fritz_profiles) <= 0:
+ _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEVICEPROFILE)
+ return []
+
+ return [
+ FritzBoxProfileSwitch(fritzbox_tools, device_friendly_name, profile)
+ for profile in fritzbox_tools.fritz_profiles.keys()
+ ]
+
+
+def wifi_entities_list(
+ fritzbox_tools: FritzBoxTools, device_friendly_name: str
+) -> list[FritzBoxWifiSwitch]:
+ """Get list of wifi entities."""
+ _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_WIFINETWORK)
+ std_table = {"ax": "Wifi6", "ac": "5Ghz", "n": "2.4Ghz"}
+ networks: dict = {}
+ for i in range(4):
+ if not ("WLANConfiguration" + str(i)) in fritzbox_tools.connection.services:
+ continue
+
+ network_info = service_call_action(
+ fritzbox_tools, "WLANConfiguration", str(i), "GetInfo"
+ )
+ if network_info:
+ ssid = network_info["NewSSID"]
+ if ssid in networks.values():
+ networks[i] = f'{ssid} {std_table[network_info["NewStandard"]]}'
+ else:
+ networks[i] = ssid
+
+ return [
+ FritzBoxWifiSwitch(fritzbox_tools, device_friendly_name, net, networks[net])
+ for net in networks
+ ]
+
+
+def all_entities_list(
+ fritzbox_tools: FritzBoxTools, device_friendly_name: str
+) -> list[Entity]:
+ """Get a list of all entities."""
+ return [
+ *deflection_entities_list(fritzbox_tools, device_friendly_name),
+ *port_entities_list(fritzbox_tools, device_friendly_name),
+ *profile_entities_list(fritzbox_tools, device_friendly_name),
+ *wifi_entities_list(fritzbox_tools, device_friendly_name),
+ ]
+
+
+async def async_setup_entry(
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
+ """Set up entry."""
+ _LOGGER.debug("Setting up switches")
+ fritzbox_tools: FritzBoxTools = hass.data[DOMAIN][entry.entry_id]
+
+ _LOGGER.debug("Fritzbox services: %s", fritzbox_tools.connection.services)
+
+ entities_list = await hass.async_add_executor_job(
+ all_entities_list, fritzbox_tools, entry.title
+ )
+ async_add_entities(entities_list)
+
+
+class FritzBoxBaseSwitch(FritzBoxBaseEntity):
+ """Fritz switch base class."""
+
+ def __init__(
+ self,
+ fritzbox_tools: FritzBoxTools,
+ device_friendly_name: str,
+ switch_info: SwitchInfo,
+ ) -> None:
+ """Init Fritzbox port switch."""
+ super().__init__(fritzbox_tools, device_friendly_name)
+
+ self._description = switch_info["description"]
+ self._friendly_name = switch_info["friendly_name"]
+ self._icon = switch_info["icon"]
+ self._type = switch_info["type"]
+ self._update = switch_info["callback_update"]
+ self._switch = switch_info["callback_switch"]
+
+ self._name = f"{self._friendly_name} {self._description}"
+ self._unique_id = (
+ f"{self._fritzbox_tools.unique_id}-{slugify(self._description)}"
+ )
+
+ self._attributes: dict[str, str] = {}
+ self._is_available = True
+
+ self._attr_is_on = False
+
+ @property
+ def name(self) -> str:
+ """Return name."""
+ return self._name
+
+ @property
+ def icon(self) -> str:
+ """Return name."""
+ return self._icon
+
+ @property
+ def unique_id(self) -> str:
+ """Return unique id."""
+ return self._unique_id
+
+ @property
+ def available(self) -> bool:
+ """Return availability."""
+ return self._is_available
+
+ @property
+ def extra_state_attributes(self) -> dict[str, str]:
+ """Return device attributes."""
+ return self._attributes
+
+ async def async_update(self) -> None:
+ """Update data."""
+ _LOGGER.debug("Updating '%s' (%s) switch state", self.name, self._type)
+ await self._update()
+
+ async def async_turn_on(self, **kwargs: Any) -> None:
+ """Turn on switch."""
+ await self._async_handle_turn_on_off(turn_on=True)
+
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Turn off switch."""
+ await self._async_handle_turn_on_off(turn_on=False)
+
+ async def _async_handle_turn_on_off(self, turn_on: bool) -> bool:
+ """Handle switch state change request."""
+ await self._switch(turn_on)
+ self._attr_is_on = turn_on
+ return True
+
+
+class FritzBoxPortSwitch(FritzBoxBaseSwitch, SwitchEntity):
+ """Defines a FRITZ!Box Tools PortForward switch."""
+
+ def __init__(
+ self,
+ fritzbox_tools: FritzBoxTools,
+ device_friendly_name: str,
+ port_mapping: dict[str, Any] | None,
+ idx: int,
+ connection_type: str,
+ ) -> None:
+ """Init Fritzbox port switch."""
+ self._fritzbox_tools = fritzbox_tools
+
+ self._attributes = {}
+ self.connection_type = connection_type
+ self.port_mapping = port_mapping # dict in the format as it comes from fritzconnection. eg: {'NewRemoteHost': '0.0.0.0', 'NewExternalPort': 22, 'NewProtocol': 'TCP', 'NewInternalPort': 22, 'NewInternalClient': '192.168.178.31', 'NewEnabled': True, 'NewPortMappingDescription': 'Beast SSH ', 'NewLeaseDuration': 0}
+ self._idx = idx # needed for update routine
+
+ if port_mapping is None:
+ return
+
+ switch_info = SwitchInfo(
+ description=f'Port forward {port_mapping["NewPortMappingDescription"]}',
+ friendly_name=device_friendly_name,
+ icon="mdi:check-network",
+ type=SWITCH_TYPE_PORTFORWARD,
+ callback_update=self._async_fetch_update,
+ callback_switch=self._async_handle_port_switch_on_off,
+ )
+ super().__init__(fritzbox_tools, device_friendly_name, switch_info)
+
+ async def _async_fetch_update(self) -> None:
+ """Fetch updates."""
+
+ self.port_mapping = await async_service_call_action(
+ self._fritzbox_tools,
+ self.connection_type,
+ "1",
+ "GetGenericPortMappingEntry",
+ NewPortMappingIndex=self._idx,
+ )
+ _LOGGER.debug(
+ "Specific %s response: %s", SWITCH_TYPE_PORTFORWARD, self.port_mapping
+ )
+ if self.port_mapping is None:
+ self._is_available = False
+ return
+
+ self._attr_is_on = self.port_mapping["NewEnabled"] is True
+ self._is_available = True
+
+ attributes_dict = {
+ "NewInternalClient": "internalIP",
+ "NewInternalPort": "internalPort",
+ "NewExternalPort": "externalPort",
+ "NewProtocol": "protocol",
+ "NewPortMappingDescription": "description",
+ }
+
+ for key in attributes_dict:
+ self._attributes[attributes_dict[key]] = self.port_mapping[key]
+
+ async def _async_handle_port_switch_on_off(self, turn_on: bool) -> bool:
+
+ if self.port_mapping is None:
+ return False
+
+ self.port_mapping["NewEnabled"] = "1" if turn_on else "0"
+
+ resp = await async_service_call_action(
+ self._fritzbox_tools,
+ self.connection_type,
+ "1",
+ "AddPortMapping",
+ **self.port_mapping,
+ )
+
+ return bool(resp is not None)
+
+
+class FritzBoxDeflectionSwitch(FritzBoxBaseSwitch, SwitchEntity):
+ """Defines a FRITZ!Box Tools PortForward switch."""
+
+ def __init__(
+ self,
+ fritzbox_tools: FritzBoxTools,
+ device_friendly_name: str,
+ dict_of_deflection: Any,
+ ) -> None:
+ """Init Fritxbox Deflection class."""
+ self._fritzbox_tools: FritzBoxTools = fritzbox_tools
+
+ self.dict_of_deflection = dict_of_deflection
+ self._attributes = {}
+ self.id = int(self.dict_of_deflection["DeflectionId"])
+
+ switch_info = SwitchInfo(
+ description=f"Call deflection {self.id}",
+ friendly_name=device_friendly_name,
+ icon="mdi:phone-forward",
+ type=SWITCH_TYPE_DEFLECTION,
+ callback_update=self._async_fetch_update,
+ callback_switch=self._async_switch_on_off_executor,
+ )
+ super().__init__(self._fritzbox_tools, device_friendly_name, switch_info)
+
+ async def _async_fetch_update(self) -> None:
+ """Fetch updates."""
+
+ resp = await async_service_call_action(
+ self._fritzbox_tools, "X_AVM-DE_OnTel", "1", "GetDeflections"
+ )
+ if not resp:
+ self._is_available = False
+ return
+
+ self.dict_of_deflection = xmltodict.parse(resp["NewDeflectionList"])["List"][
+ "Item"
+ ]
+ if isinstance(self.dict_of_deflection, list):
+ self.dict_of_deflection = self.dict_of_deflection[self.id]
+
+ _LOGGER.debug(
+ "Specific %s response: NewDeflectionList=%s",
+ SWITCH_TYPE_DEFLECTION,
+ self.dict_of_deflection,
+ )
+
+ self._attr_is_on = self.dict_of_deflection["Enable"] == "1"
+ self._is_available = True
+
+ self._attributes["Type"] = self.dict_of_deflection["Type"]
+ self._attributes["Number"] = self.dict_of_deflection["Number"]
+ self._attributes["DeflectionToNumber"] = self.dict_of_deflection[
+ "DeflectionToNumber"
+ ]
+ # Return mode sample: "eImmediately"
+ self._attributes["Mode"] = self.dict_of_deflection["Mode"][1:]
+ self._attributes["Outgoing"] = self.dict_of_deflection["Outgoing"]
+ self._attributes["PhonebookID"] = self.dict_of_deflection["PhonebookID"]
+
+ async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
+ """Handle deflection switch."""
+ await async_service_call_action(
+ self._fritzbox_tools,
+ "X_AVM-DE_OnTel",
+ "1",
+ "SetDeflectionEnable",
+ NewDeflectionId=self.id,
+ NewEnable="1" if turn_on else "0",
+ )
+
+
+class FritzBoxProfileSwitch(FritzBoxBaseSwitch, SwitchEntity):
+ """Defines a FRITZ!Box Tools DeviceProfile switch."""
+
+ def __init__(
+ self, fritzbox_tools: FritzBoxTools, device_friendly_name: str, profile: str
+ ) -> None:
+ """Init Fritz profile."""
+ self._fritzbox_tools: FritzBoxTools = fritzbox_tools
+ self.profile = profile
+
+ switch_info = SwitchInfo(
+ description=f"Profile {profile}",
+ friendly_name=device_friendly_name,
+ icon="mdi:router-wireless-settings",
+ type=SWITCH_TYPE_DEVICEPROFILE,
+ callback_update=self._async_fetch_update,
+ callback_switch=self._async_switch_on_off_executor,
+ )
+ super().__init__(self._fritzbox_tools, device_friendly_name, switch_info)
+
+ async def _async_fetch_update(self) -> None:
+ """Update data."""
+ try:
+ status = await self.hass.async_add_executor_job(
+ self._fritzbox_tools.fritz_profiles[self.profile].get_state
+ )
+ _LOGGER.debug(
+ "Specific %s response: get_State()=%s",
+ SWITCH_TYPE_DEVICEPROFILE,
+ status,
+ )
+ if status == SWITCH_PROFILE_STATUS_OFF:
+ self._attr_is_on = False
+ self._is_available = True
+ elif status == SWITCH_PROFILE_STATUS_ON:
+ self._attr_is_on = True
+ self._is_available = True
+ else:
+ self._is_available = False
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.error("Could not get %s state", self.name, exc_info=True)
+ self._is_available = False
+
+ async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
+ """Handle profile switch."""
+ state = SWITCH_PROFILE_STATUS_ON if turn_on else SWITCH_PROFILE_STATUS_OFF
+ await self.hass.async_add_executor_job(
+ self._fritzbox_tools.fritz_profiles[self.profile].set_state, state
+ )
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Return if the entity should be enabled when first added to the entity registry."""
+ return False
+
+
+class FritzBoxWifiSwitch(FritzBoxBaseSwitch, SwitchEntity):
+ """Defines a FRITZ!Box Tools Wifi switch."""
+
+ def __init__(
+ self,
+ fritzbox_tools: FritzBoxTools,
+ device_friendly_name: str,
+ network_num: int,
+ network_name: str,
+ ) -> None:
+ """Init Fritz Wifi switch."""
+ self._fritzbox_tools = fritzbox_tools
+
+ self._attributes = {}
+ self._network_num = network_num
+
+ switch_info = SwitchInfo(
+ description=f"Wi-Fi {network_name}",
+ friendly_name=device_friendly_name,
+ icon="mdi:wifi",
+ type=SWITCH_TYPE_WIFINETWORK,
+ callback_update=self._async_fetch_update,
+ callback_switch=self._async_switch_on_off_executor,
+ )
+ super().__init__(self._fritzbox_tools, device_friendly_name, switch_info)
+
+ async def _async_fetch_update(self) -> None:
+ """Fetch updates."""
+
+ wifi_info = await async_service_call_action(
+ self._fritzbox_tools,
+ "WLANConfiguration",
+ str(self._network_num),
+ "GetInfo",
+ )
+ _LOGGER.debug(
+ "Specific %s response: GetInfo=%s", SWITCH_TYPE_WIFINETWORK, wifi_info
+ )
+
+ if wifi_info is None:
+ self._is_available = False
+ return
+
+ self._attr_is_on = wifi_info["NewEnable"] is True
+ self._is_available = True
+
+ std = wifi_info["NewStandard"]
+ self._attributes["standard"] = std if std else None
+ self._attributes["BSSID"] = wifi_info["NewBSSID"]
+ self._attributes["mac_address_control"] = wifi_info[
+ "NewMACAddressControlEnabled"
+ ]
+
+ async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
+ """Handle wifi switch."""
+ await async_service_call_action(
+ self._fritzbox_tools,
+ "WLANConfiguration",
+ str(self._network_num),
+ "SetEnable",
+ NewEnable="1" if turn_on else "0",
+ )
diff --git a/homeassistant/components/fritz/translations/ca.json b/homeassistant/components/fritz/translations/ca.json
index 10926ed8348..2e240bb9833 100644
--- a/homeassistant/components/fritz/translations/ca.json
+++ b/homeassistant/components/fritz/translations/ca.json
@@ -51,5 +51,14 @@
"title": "Configuraci\u00f3 de FRITZ!Box Tools"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Segons d'espera abans de considerar un dispositiu a 'casa'"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json
index d620396f1b5..dcded6750e9 100644
--- a/homeassistant/components/fritz/translations/de.json
+++ b/homeassistant/components/fritz/translations/de.json
@@ -12,23 +12,23 @@
"connection_error": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung"
},
- "flow_title": "FRITZ! Box Tools: {name}",
+ "flow_title": "FRITZ!Box Tools: {name}",
"step": {
"confirm": {
"data": {
"password": "Passwort",
"username": "Benutzername"
},
- "description": "Entdeckte FRITZ! Box: {name} \n\nRichte deine FRITZ! Box Tools ein, um {name} zu kontrollieren",
- "title": "FRITZ! Box Tools einrichten"
+ "description": "Entdeckte FRITZ!Box: {name} \n\nRichte deine FRITZ!Box Tools ein, um {name} zu kontrollieren",
+ "title": "FRITZ!Box Tools einrichten"
},
"reauth_confirm": {
"data": {
"password": "Passwort",
"username": "Benutzername"
},
- "description": "Aktualisiere die Anmeldeinformationen von FRITZ! Box Tools f\u00fcr: {host}. \n\nFRITZ! Box Tools kann sich nicht bei deiner FRITZ! Box anmelden.",
- "title": "Aktualisieren der FRITZ! Box Tools - Anmeldeinformationen"
+ "description": "Aktualisiere die Anmeldeinformationen von FRITZ!Box Tools f\u00fcr: {host}. \n\nFRITZ!Box Tools kann sich nicht an deiner FRITZ!Box anmelden.",
+ "title": "Aktualisieren der FRITZ!Box Tools - Anmeldeinformationen"
},
"start_config": {
"data": {
@@ -37,8 +37,8 @@
"port": "Port",
"username": "Benutzername"
},
- "description": "Einrichten der FRITZ! Box Tools zur Steuerung Ihrer FRITZ! Box.\n Ben\u00f6tigt: Benutzername, Passwort.",
- "title": "Setup FRITZ! Box Tools - obligatorisch"
+ "description": "Einrichten der FRITZ!Box Tools zur Steuerung Ihrer FRITZ!Box.\n Ben\u00f6tigt: Benutzername, Passwort.",
+ "title": "Setup FRITZ!Box Tools - obligatorisch"
},
"user": {
"data": {
@@ -46,6 +46,17 @@
"password": "Passwort",
"port": "Port",
"username": "Benutzername"
+ },
+ "description": "FRITZ!Box Tools einrichten, um Ihre FRITZ!Box zu steuern.\nMindestens erforderlich: Benutzername, Passwort.",
+ "title": "Setup FRITZ!Box Tools"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Sekunden, um ein Ger\u00e4t als 'zu Hause' zu betrachten"
}
}
}
diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json
index db9b2fa5c2a..ed39b227ec8 100644
--- a/homeassistant/components/fritz/translations/es.json
+++ b/homeassistant/components/fritz/translations/es.json
@@ -38,6 +38,19 @@
},
"description": "Configurar FRITZ!Box Tools para controlar tu FRITZ!Box.\nM\u00ednimo necesario: usuario, contrase\u00f1a.",
"title": "Configurar FRITZ!Box Tools - obligatorio"
+ },
+ "user": {
+ "description": "Configure las herramientas de FRITZ! Box para controlar su FRITZ! Box.\n M\u00ednimo necesario: nombre de usuario, contrase\u00f1a.",
+ "title": "Configurar las herramientas de FRITZ! Box"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Segundos para considerar un dispositivo en 'casa'"
+ }
}
}
}
diff --git a/homeassistant/components/fritz/translations/he.json b/homeassistant/components/fritz/translations/he.json
new file mode 100644
index 00000000000..783f215cc40
--- /dev/null
+++ b/homeassistant/components/fritz/translations/he.json
@@ -0,0 +1,56 @@
+{
+ "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",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "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",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "connection_error": "\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"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "start_config": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "\u05e9\u05e0\u05d9\u05d5\u05ea \u05db\u05d3\u05d9 \u05dc\u05d4\u05d7\u05e9\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df \u05d1'\u05d1\u05d9\u05ea'"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritz/translations/hu.json b/homeassistant/components/fritz/translations/hu.json
new file mode 100644
index 00000000000..eda37325071
--- /dev/null
+++ b/homeassistant/components/fritz/translations/hu.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json
index f39b8bc7a7f..0169d275205 100644
--- a/homeassistant/components/fritz/translations/it.json
+++ b/homeassistant/components/fritz/translations/it.json
@@ -51,5 +51,14 @@
"title": "Configura gli strumenti del FRITZ!Box"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Secondi per considerare un dispositivo \"a casa\""
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/fritz/translations/pl.json b/homeassistant/components/fritz/translations/pl.json
index b9f17e23624..5632ae67694 100644
--- a/homeassistant/components/fritz/translations/pl.json
+++ b/homeassistant/components/fritz/translations/pl.json
@@ -8,6 +8,7 @@
"error": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"already_in_progress": "Konfiguracja jest ju\u017c w toku",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"invalid_auth": "Niepoprawne uwierzytelnienie"
},
@@ -38,6 +39,25 @@
},
"description": "Skonfiguruj narz\u0119dzia FRITZ!Box, aby sterowa\u0107 urz\u0105dzeniem FRITZ! Box.\nMinimalne wymagania: nazwa u\u017cytkownika, has\u0142o.",
"title": "Konfiguracja narz\u0119dzi FRITZ!Box - obowi\u0105zkowe"
+ },
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "password": "Has\u0142o",
+ "port": "Port",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "description": "Skonfiguruj narz\u0119dzia FRITZ!Box, aby sterowa\u0107 urz\u0105dzeniem FRITZ!Box.\nMinimalne wymagania: nazwa u\u017cytkownika, has\u0142o.",
+ "title": "Konfiguracja narz\u0119dzi FRITZ!Box"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Czas w sekundach, zanim urz\u0105dzenie otrzyma stan \"w domu\""
+ }
}
}
}
diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json
index 16263722482..ceaca6fd19a 100644
--- a/homeassistant/components/fritzbox/translations/de.json
+++ b/homeassistant/components/fritzbox/translations/de.json
@@ -4,13 +4,13 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
"already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
"no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden",
- "not_supported": "Verbunden mit AVM FRITZ! Box, kann jedoch keine Smart Home-Ger\u00e4te steuern.",
+ "not_supported": "Verbunden mit AVM FRITZ!Box, kann jedoch keine Smart Home-Ger\u00e4te steuern.",
"reauth_successful": "Die erneute Authentifizierung war erfolgreich"
},
"error": {
"invalid_auth": "Ung\u00fcltige Zugangsdaten"
},
- "flow_title": "AVM FRITZ! Box: {name}",
+ "flow_title": "AVM FRITZ!Box: {name}",
"step": {
"confirm": {
"data": {
@@ -32,7 +32,7 @@
"password": "Passwort",
"username": "Benutzername"
},
- "description": "Gib deine AVM FRITZ! Box-Informationen ein."
+ "description": "Gib deine AVM FRITZ!Box-Informationen ein."
}
}
}
diff --git a/homeassistant/components/fritzbox/translations/he.json b/homeassistant/components/fritzbox/translations/he.json
index 035cb07a170..ec9248b5ea6 100644
--- a/homeassistant/components/fritzbox/translations/he.json
+++ b/homeassistant/components/fritzbox/translations/he.json
@@ -1,5 +1,15 @@
{
"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",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "flow_title": "{name}",
"step": {
"confirm": {
"data": {
@@ -7,8 +17,15 @@
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
},
+ "reauth_confirm": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
"user": {
"data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json
index 44b68d5f540..630b15b990c 100644
--- a/homeassistant/components/fritzbox/translations/hu.json
+++ b/homeassistant/components/fritzbox/translations/hu.json
@@ -9,7 +9,7 @@
"error": {
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
- "flow_title": "AVM FRITZ!Box: {name}",
+ "flow_title": "{name}",
"step": {
"confirm": {
"data": {
diff --git a/homeassistant/components/fritzbox_callmonitor/base.py b/homeassistant/components/fritzbox_callmonitor/base.py
index af0612d7632..0db40e2098f 100644
--- a/homeassistant/components/fritzbox_callmonitor/base.py
+++ b/homeassistant/components/fritzbox_callmonitor/base.py
@@ -8,7 +8,7 @@ from fritzconnection.lib.fritzphonebook import FritzPhonebook
from homeassistant.util import Throttle
-from .const import REGEX_NUMBER, UNKOWN_NAME
+from .const import REGEX_NUMBER, UNKNOWN_NAME
_LOGGER = logging.getLogger(__name__)
@@ -61,13 +61,13 @@ class FritzBoxPhonebook:
"""Return a name for a given phone number."""
number = re.sub(REGEX_NUMBER, "", str(number))
if self.number_dict is None:
- return UNKOWN_NAME
+ return UNKNOWN_NAME
if number in self.number_dict:
return self.number_dict[number]
if not self.prefixes:
- return UNKOWN_NAME
+ return UNKNOWN_NAME
for prefix in self.prefixes:
with suppress(KeyError):
diff --git a/homeassistant/components/fritzbox_callmonitor/const.py b/homeassistant/components/fritzbox_callmonitor/const.py
index a71f14401b3..ba0f8d1d973 100644
--- a/homeassistant/components/fritzbox_callmonitor/const.py
+++ b/homeassistant/components/fritzbox_callmonitor/const.py
@@ -19,7 +19,7 @@ FRITZ_ATTR_NAME = "name"
FRITZ_ATTR_SERIAL_NUMBER = "NewSerialNumber"
FRITZ_SERVICE_DEVICE_INFO = "DeviceInfo"
-UNKOWN_NAME = "unknown"
+UNKNOWN_NAME = "unknown"
SERIAL_NUMBER = "serial_number"
REGEX_NUMBER = r"[^\d\+]"
diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py
index a325c0ca71d..63b3cd81aa5 100644
--- a/homeassistant/components/fritzbox_callmonitor/sensor.py
+++ b/homeassistant/components/fritzbox_callmonitor/sensor.py
@@ -42,7 +42,7 @@ from .const import (
STATE_IDLE,
STATE_RINGING,
STATE_TALKING,
- UNKOWN_NAME,
+ UNKNOWN_NAME,
)
_LOGGER = logging.getLogger(__name__)
@@ -193,7 +193,7 @@ class FritzBoxCallSensor(SensorEntity):
def number_to_name(self, number):
"""Return a name for a given phone number."""
if self._fritzbox_phonebook is None:
- return UNKOWN_NAME
+ return UNKNOWN_NAME
return self._fritzbox_phonebook.get_name(number)
def update(self):
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/de.json b/homeassistant/components/fritzbox_callmonitor/translations/de.json
index a26f301a9bd..b48ec34a030 100644
--- a/homeassistant/components/fritzbox_callmonitor/translations/de.json
+++ b/homeassistant/components/fritzbox_callmonitor/translations/de.json
@@ -8,7 +8,7 @@
"error": {
"invalid_auth": "Ung\u00fcltige Authentifizierung"
},
- "flow_title": "AVM FRITZ! Box-Anrufmonitor: {name}",
+ "flow_title": "AVM FRITZ!Box-Anrufmonitor: {name}",
"step": {
"phonebook": {
"data": {
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/he.json b/homeassistant/components/fritzbox_callmonitor/translations/he.json
new file mode 100644
index 00000000000..7951a71054c
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/he.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "\u05e1\u05e4\u05e8 \u05d8\u05dc\u05e4\u05d5\u05e0\u05d9\u05dd"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/hu.json b/homeassistant/components/fritzbox_callmonitor/translations/hu.json
index 8c2c34775e5..3255d205fa1 100644
--- a/homeassistant/components/fritzbox_callmonitor/translations/hu.json
+++ b/homeassistant/components/fritzbox_callmonitor/translations/hu.json
@@ -7,6 +7,7 @@
"error": {
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
+ "flow_title": "{name}",
"step": {
"phonebook": {
"data": {
diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json
index 4f48bc1aecc..d526fc90b32 100644
--- a/homeassistant/components/fronius/manifest.json
+++ b/homeassistant/components/fronius/manifest.json
@@ -2,7 +2,7 @@
"domain": "fronius",
"name": "Fronius",
"documentation": "https://www.home-assistant.io/integrations/fronius",
- "requirements": ["pyfronius==0.4.6"],
+ "requirements": ["pyfronius==0.5.2"],
"codeowners": ["@nielstron"],
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 1b104982026..392806dc885 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -1,13 +1,14 @@
"""Handle the frontend for Home Assistant."""
from __future__ import annotations
+from collections.abc import Iterator
from functools import lru_cache
import json
import logging
import mimetypes
import os
import pathlib
-from typing import Any
+from typing import Any, TypedDict, cast
from aiohttp import hdrs, web, web_urldispatcher
import jinja2
@@ -16,18 +17,18 @@ from yarl import URL
from homeassistant.components import websocket_api
from homeassistant.components.http.view import HomeAssistantView
+from homeassistant.components.websocket_api.connection import ActiveConnection
from homeassistant.config import async_hass_config_yaml
from homeassistant.const import CONF_MODE, CONF_NAME, EVENT_THEMES_UPDATED
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import service
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.translation import async_get_translations
+from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration, bind_hass
from .storage import async_setup_frontend_storage
-# mypy: allow-untyped-defs, no-check-untyped-defs
-
# Fix mimetypes for borked Windows machines
# https://github.com/home-assistant/frontend/issues/3336
mimetypes.add_type("text/css", ".css")
@@ -191,15 +192,15 @@ class UrlManager:
on hass.data
"""
- def __init__(self, urls):
+ def __init__(self, urls: list[str]) -> None:
"""Init the url manager."""
self.urls = frozenset(urls)
- def add(self, url):
+ def add(self, url: str) -> None:
"""Add a url to the set."""
self.urls = frozenset([*self.urls, url])
- def remove(self, url):
+ def remove(self, url: str) -> None:
"""Remove a url from the set."""
self.urls = self.urls - {url}
@@ -208,7 +209,7 @@ class Panel:
"""Abstract class for panels."""
# Name of the webcomponent
- component_name: str | None = None
+ component_name: str
# Icon to show in the sidebar
sidebar_icon: str | None = None
@@ -227,13 +228,13 @@ class Panel:
def __init__(
self,
- component_name,
- sidebar_title,
- sidebar_icon,
- frontend_url_path,
- config,
- require_admin,
- ):
+ component_name: str,
+ sidebar_title: str | None,
+ sidebar_icon: str | None,
+ frontend_url_path: str | None,
+ config: dict[str, Any] | None,
+ require_admin: bool,
+ ) -> None:
"""Initialize a built-in panel."""
self.component_name = component_name
self.sidebar_title = sidebar_title
@@ -243,7 +244,7 @@ class Panel:
self.require_admin = require_admin
@callback
- def to_response(self):
+ def to_response(self) -> PanelRespons:
"""Panel as dictionary."""
return {
"component_name": self.component_name,
@@ -258,16 +259,16 @@ class Panel:
@bind_hass
@callback
def async_register_built_in_panel(
- hass,
- component_name,
- sidebar_title=None,
- sidebar_icon=None,
- frontend_url_path=None,
- config=None,
- require_admin=False,
+ hass: HomeAssistant,
+ component_name: str,
+ sidebar_title: str | None = None,
+ sidebar_icon: str | None = None,
+ frontend_url_path: str | None = None,
+ config: dict[str, Any] | None = None,
+ require_admin: bool = False,
*,
- update=False,
-):
+ update: bool = False,
+) -> None:
"""Register a built-in panel."""
panel = Panel(
component_name,
@@ -290,7 +291,7 @@ def async_register_built_in_panel(
@bind_hass
@callback
-def async_remove_panel(hass, frontend_url_path):
+def async_remove_panel(hass: HomeAssistant, frontend_url_path: str) -> None:
"""Remove a built-in panel."""
panel = hass.data.get(DATA_PANELS, {}).pop(frontend_url_path, None)
@@ -300,18 +301,18 @@ def async_remove_panel(hass, frontend_url_path):
hass.bus.async_fire(EVENT_PANELS_UPDATED)
-def add_extra_js_url(hass, url, es5=False):
+def add_extra_js_url(hass: HomeAssistant, url: str, es5: bool = False) -> None:
"""Register extra js or module url to load."""
key = DATA_EXTRA_JS_URL_ES5 if es5 else DATA_EXTRA_MODULE_URL
hass.data[key].add(url)
-def add_manifest_json_key(key, val):
+def add_manifest_json_key(key: str, val: Any) -> None:
"""Add a keyval to the manifest.json."""
MANIFEST_JSON.update_key(key, val)
-def _frontend_root(dev_repo_path):
+def _frontend_root(dev_repo_path: str | None) -> pathlib.Path:
"""Return root path to the frontend files."""
if dev_repo_path is not None:
return pathlib.Path(dev_repo_path) / "hass_frontend"
@@ -319,17 +320,17 @@ def _frontend_root(dev_repo_path):
# pylint: disable=import-outside-toplevel
import hass_frontend
- return hass_frontend.where()
+ return cast(pathlib.Path, hass_frontend.where())
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the serving of the frontend."""
await async_setup_frontend_storage(hass)
hass.components.websocket_api.async_register_command(websocket_get_panels)
hass.components.websocket_api.async_register_command(websocket_get_themes)
hass.components.websocket_api.async_register_command(websocket_get_translations)
hass.components.websocket_api.async_register_command(websocket_get_version)
- hass.http.register_view(ManifestJSONView)
+ hass.http.register_view(ManifestJSONView())
conf = config.get(DOMAIN, {})
@@ -396,7 +397,9 @@ async def async_setup(hass, config):
return True
-async def _async_setup_themes(hass, themes):
+async def _async_setup_themes(
+ hass: HomeAssistant, themes: dict[str, Any] | None
+) -> None:
"""Set up themes data and services."""
hass.data[DATA_THEMES] = themes or {}
@@ -417,7 +420,7 @@ async def _async_setup_themes(hass, themes):
hass.data[DATA_DEFAULT_DARK_THEME] = dark_theme_name
@callback
- def update_theme_and_fire_event():
+ def update_theme_and_fire_event() -> None:
"""Update theme_color in manifest."""
name = hass.data[DATA_DEFAULT_THEME]
themes = hass.data[DATA_THEMES]
@@ -434,7 +437,7 @@ async def _async_setup_themes(hass, themes):
hass.bus.async_fire(EVENT_THEMES_UPDATED)
@callback
- def set_theme(call):
+ def set_theme(call: ServiceCall) -> None:
"""Set backend-preferred theme."""
name = call.data[CONF_NAME]
mode = call.data.get("mode", "light")
@@ -466,7 +469,7 @@ async def _async_setup_themes(hass, themes):
)
update_theme_and_fire_event()
- async def reload_themes(_):
+ async def reload_themes(_: ServiceCall) -> None:
"""Reload themes."""
config = await async_hass_config_yaml(hass)
new_themes = config[DOMAIN].get(CONF_THEMES, {})
@@ -500,19 +503,19 @@ async def _async_setup_themes(hass, themes):
@callback
@lru_cache(maxsize=1)
-def _async_render_index_cached(template, **kwargs):
+def _async_render_index_cached(template: jinja2.Template, **kwargs: Any) -> str:
return template.render(**kwargs)
class IndexView(web_urldispatcher.AbstractResource):
"""Serve the frontend."""
- def __init__(self, repo_path, hass):
+ def __init__(self, repo_path: str | None, hass: HomeAssistant) -> None:
"""Initialize the frontend view."""
super().__init__(name="frontend:index")
self.repo_path = repo_path
self.hass = hass
- self._template_cache = None
+ self._template_cache: jinja2.Template | None = None
@property
def canonical(self) -> str:
@@ -520,7 +523,7 @@ class IndexView(web_urldispatcher.AbstractResource):
return "/"
@property
- def _route(self):
+ def _route(self) -> web_urldispatcher.ResourceRoute:
"""Return the index route."""
return web_urldispatcher.ResourceRoute("GET", self.get, self)
@@ -552,7 +555,7 @@ class IndexView(web_urldispatcher.AbstractResource):
Required for subapplications support.
"""
- def get_info(self):
+ def get_info(self) -> dict[str, list[str]]: # type: ignore[override]
"""Return a dict with additional info useful for introspection."""
return {"panels": list(self.hass.data[DATA_PANELS])}
@@ -562,7 +565,7 @@ class IndexView(web_urldispatcher.AbstractResource):
def raw_match(self, path: str) -> bool:
"""Perform a raw match against path."""
- def get_template(self):
+ def get_template(self) -> jinja2.Template:
"""Get template."""
tpl = self._template_cache
if tpl is None:
@@ -600,7 +603,7 @@ class IndexView(web_urldispatcher.AbstractResource):
"""Return length of resource."""
return 1
- def __iter__(self):
+ def __iter__(self) -> Iterator[web_urldispatcher.ResourceRoute]:
"""Iterate over routes."""
return iter([self._route])
@@ -613,7 +616,7 @@ class ManifestJSONView(HomeAssistantView):
name = "manifestjson"
@callback
- def get(self, request): # pylint: disable=no-self-use
+ def get(self, request: web.Request) -> web.Response: # pylint: disable=no-self-use
"""Return the manifest.json."""
return web.Response(
text=MANIFEST_JSON.json, content_type="application/manifest+json"
@@ -622,7 +625,9 @@ class ManifestJSONView(HomeAssistantView):
@callback
@websocket_api.websocket_command({"type": "get_panels"})
-def websocket_get_panels(hass, connection, msg):
+def websocket_get_panels(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+) -> None:
"""Handle get panels command."""
user_is_admin = connection.user.is_admin
panels = {
@@ -636,7 +641,9 @@ def websocket_get_panels(hass, connection, msg):
@callback
@websocket_api.websocket_command({"type": "frontend/get_themes"})
-def websocket_get_themes(hass, connection, msg):
+def websocket_get_themes(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+) -> None:
"""Handle get themes command."""
if hass.config.safe_mode:
connection.send_message(
@@ -677,7 +684,9 @@ def websocket_get_themes(hass, connection, msg):
}
)
@websocket_api.async_response
-async def websocket_get_translations(hass, connection, msg):
+async def websocket_get_translations(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+) -> None:
"""Handle get translations command."""
resources = await async_get_translations(
hass,
@@ -693,7 +702,9 @@ async def websocket_get_translations(hass, connection, msg):
@websocket_api.websocket_command({"type": "frontend/get_version"})
@websocket_api.async_response
-async def websocket_get_version(hass, connection, msg):
+async def websocket_get_version(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+) -> None:
"""Handle get version command."""
integration = await async_get_integration(hass, "frontend")
@@ -707,3 +718,14 @@ async def websocket_get_version(hass, connection, msg):
connection.send_error(msg["id"], "unknown_version", "Version not found")
else:
connection.send_result(msg["id"], {"version": frontend})
+
+
+class PanelRespons(TypedDict):
+ """Represent the panel response type."""
+
+ component_name: str
+ icon: str | None
+ title: str | None
+ config: dict[str, Any] | None
+ url_path: str | None
+ require_admin: bool
diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json
index 8f5f5f091d9..7af6e2bc733 100644
--- a/homeassistant/components/frontend/manifest.json
+++ b/homeassistant/components/frontend/manifest.json
@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
- "home-assistant-frontend==20210603.0"
+ "home-assistant-frontend==20210707.0"
],
"dependencies": [
"api",
diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py
index b37945b5e07..294b707c965 100644
--- a/homeassistant/components/frontend/storage.py
+++ b/homeassistant/components/frontend/storage.py
@@ -1,28 +1,34 @@
"""API for persistent storage for the frontend."""
+from __future__ import annotations
+
from functools import wraps
+from typing import Any, Callable
import voluptuous as vol
from homeassistant.components import websocket_api
-
-# mypy: allow-untyped-calls, allow-untyped-defs
+from homeassistant.components.websocket_api.connection import ActiveConnection
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.storage import Store
DATA_STORAGE = "frontend_storage"
STORAGE_VERSION_USER_DATA = 1
-async def async_setup_frontend_storage(hass):
+async def async_setup_frontend_storage(hass: HomeAssistant) -> None:
"""Set up frontend storage."""
hass.data[DATA_STORAGE] = ({}, {})
hass.components.websocket_api.async_register_command(websocket_set_user_data)
hass.components.websocket_api.async_register_command(websocket_get_user_data)
-def with_store(orig_func):
+def with_store(orig_func: Callable) -> Callable:
"""Decorate function to provide data."""
@wraps(orig_func)
- async def with_store_func(hass, connection, msg):
+ async def with_store_func(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+ ) -> None:
"""Provide user specific data and store to function."""
stores, data = hass.data[DATA_STORAGE]
user_id = connection.user.id
@@ -50,7 +56,13 @@ def with_store(orig_func):
)
@websocket_api.async_response
@with_store
-async def websocket_set_user_data(hass, connection, msg, store, data):
+async def websocket_set_user_data(
+ hass: HomeAssistant,
+ connection: ActiveConnection,
+ msg: dict,
+ store: Store,
+ data: dict[str, Any],
+) -> None:
"""Handle set global data command.
Async friendly.
@@ -65,7 +77,13 @@ async def websocket_set_user_data(hass, connection, msg, store, data):
)
@websocket_api.async_response
@with_store
-async def websocket_get_user_data(hass, connection, msg, store, data):
+async def websocket_get_user_data(
+ hass: HomeAssistant,
+ connection: ActiveConnection,
+ msg: dict,
+ store: Store,
+ data: dict[str, Any],
+) -> None:
"""Handle get global data command.
Async friendly.
diff --git a/homeassistant/components/garages_amsterdam/translations/de.json b/homeassistant/components/garages_amsterdam/translations/de.json
index 71ade34b066..aa13e22eabb 100644
--- a/homeassistant/components/garages_amsterdam/translations/de.json
+++ b/homeassistant/components/garages_amsterdam/translations/de.json
@@ -4,6 +4,15 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
"cannot_connect": "Verbindung fehlgeschlagen",
"unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "garage_name": "Name der Garage"
+ },
+ "title": "W\u00e4hlen Sie eine Garage zur \u00dcberwachung aus"
+ }
}
- }
+ },
+ "title": "Garages Amsterdam"
}
\ No newline at end of file
diff --git a/homeassistant/components/garages_amsterdam/translations/es.json b/homeassistant/components/garages_amsterdam/translations/es.json
new file mode 100644
index 00000000000..3bf5c176b56
--- /dev/null
+++ b/homeassistant/components/garages_amsterdam/translations/es.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "garage_name": "Nombre del garaje"
+ },
+ "title": "Elige un garaje para vigilar"
+ }
+ }
+ },
+ "title": "Garajes Amsterdam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/garages_amsterdam/translations/he.json b/homeassistant/components/garages_amsterdam/translations/he.json
new file mode 100644
index 00000000000..64404003783
--- /dev/null
+++ b/homeassistant/components/garages_amsterdam/translations/he.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/garages_amsterdam/translations/hu.json b/homeassistant/components/garages_amsterdam/translations/hu.json
new file mode 100644
index 00000000000..c02cd4077ba
--- /dev/null
+++ b/homeassistant/components/garages_amsterdam/translations/hu.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/garages_amsterdam/translations/pl.json b/homeassistant/components/garages_amsterdam/translations/pl.json
new file mode 100644
index 00000000000..a9f220d9bfc
--- /dev/null
+++ b/homeassistant/components/garages_amsterdam/translations/pl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "garage_name": "Nazwa parkingu"
+ },
+ "title": "Wybierz parking do monitorowania"
+ }
+ }
+ },
+ "title": "Parkingi Amsterdamie"
+}
\ No newline at end of file
diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py
index bd8920e43a8..180fcdb08a2 100644
--- a/homeassistant/components/garmin_connect/__init__.py
+++ b/homeassistant/components/garmin_connect/__init__.py
@@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Garmin Connect from a config entry."""
username: str = entry.data[CONF_USERNAME]
diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py
index eb1690c9765..96f352c75b4 100644
--- a/homeassistant/components/garmin_connect/sensor.py
+++ b/homeassistant/components/garmin_connect/sensor.py
@@ -3,7 +3,7 @@ from __future__ import annotations
import logging
-from garminconnect_aio import (
+from garminconnect_ha import (
GarminConnectAuthenticationError,
GarminConnectConnectionError,
GarminConnectTooManyRequestsError,
diff --git a/homeassistant/components/garmin_connect/translations/he.json b/homeassistant/components/garmin_connect/translations/he.json
index ac90b3264ea..e7bab78fd58 100644
--- a/homeassistant/components/garmin_connect/translations/he.json
+++ b/homeassistant/components/garmin_connect/translations/he.json
@@ -1,5 +1,14 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "too_many_requests": "\u05d1\u05e7\u05e9\u05d5\u05ea \u05e8\u05d1\u05d5\u05ea \u05de\u05d3\u05d9, \u05e0\u05d0 \u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05e0\u05d9\u05ea \u05de\u05d0\u05d5\u05d7\u05e8 \u05d9\u05d5\u05ea\u05e8.",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/gdacs/manifest.json b/homeassistant/components/gdacs/manifest.json
index 26743a69d68..65407e85848 100644
--- a/homeassistant/components/gdacs/manifest.json
+++ b/homeassistant/components/gdacs/manifest.json
@@ -3,7 +3,7 @@
"name": "Global Disaster Alert and Coordination System (GDACS)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/gdacs",
- "requirements": ["aio_georss_gdacs==0.4"],
+ "requirements": ["aio_georss_gdacs==0.5"],
"codeowners": ["@exxamalte"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"
diff --git a/homeassistant/components/gdacs/translations/he.json b/homeassistant/components/gdacs/translations/he.json
new file mode 100644
index 00000000000..48a6eeeea33
--- /dev/null
+++ b/homeassistant/components/gdacs/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/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py
index 11294e73f63..c32917cb5cd 100644
--- a/homeassistant/components/geo_location/__init__.py
+++ b/homeassistant/components/geo_location/__init__.py
@@ -5,7 +5,9 @@ from datetime import timedelta
import logging
from typing import final
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
@@ -36,14 +38,16 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class GeolocationEvent(Entity):
diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py
index 4410d39c0a6..c5e35ece593 100644
--- a/homeassistant/components/geo_location/trigger.py
+++ b/homeassistant/components/geo_location/trigger.py
@@ -18,7 +18,7 @@ EVENT_ENTER = "enter"
EVENT_LEAVE = "leave"
DEFAULT_EVENT = EVENT_ENTER
-TRIGGER_SCHEMA = vol.Schema(
+TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "geo_location",
vol.Required(CONF_SOURCE): cv.string,
@@ -37,7 +37,7 @@ def source_match(state, source):
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
source = config.get(CONF_SOURCE).lower()
zone_entity_id = config.get(CONF_ZONE)
trigger_event = config.get(CONF_EVENT)
@@ -78,6 +78,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
job,
{
"trigger": {
+ **trigger_data,
"platform": "geo_location",
"source": source,
"entity_id": event.data.get("entity_id"),
@@ -86,7 +87,6 @@ async def async_attach_trigger(hass, config, action, automation_info):
"zone": zone_state,
"event": trigger_event,
"description": f"geo_location - {source}",
- "id": trigger_id,
}
},
event.context,
diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json
index e7ac2948237..6a470e1ddbd 100644
--- a/homeassistant/components/geo_rss_events/manifest.json
+++ b/homeassistant/components/geo_rss_events/manifest.json
@@ -2,7 +2,7 @@
"domain": "geo_rss_events",
"name": "GeoRSS",
"documentation": "https://www.home-assistant.io/integrations/geo_rss_events",
- "requirements": ["georss_generic_client==0.4"],
+ "requirements": ["georss_generic_client==0.6"],
"codeowners": ["@exxamalte"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/geofency/translations/he.json b/homeassistant/components/geofency/translations/he.json
new file mode 100644
index 00000000000..ebee9aee976
--- /dev/null
+++ b/homeassistant/components/geofency/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "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.",
+ "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json
index 64a78c02d25..5668cd6cb3f 100644
--- a/homeassistant/components/geonetnz_quakes/manifest.json
+++ b/homeassistant/components/geonetnz_quakes/manifest.json
@@ -3,7 +3,7 @@
"name": "GeoNet NZ Quakes",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes",
- "requirements": ["aio_geojson_geonetnz_quakes==0.12"],
+ "requirements": ["aio_geojson_geonetnz_quakes==0.13"],
"codeowners": ["@exxamalte"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"
diff --git a/homeassistant/components/geonetnz_quakes/translations/he.json b/homeassistant/components/geonetnz_quakes/translations/he.json
new file mode 100644
index 00000000000..48a6eeeea33
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/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/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json
index ed0ebccf620..dbd793c49b3 100644
--- a/homeassistant/components/geonetnz_volcano/manifest.json
+++ b/homeassistant/components/geonetnz_volcano/manifest.json
@@ -3,7 +3,7 @@
"name": "GeoNet NZ Volcano",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/geonetnz_volcano",
- "requirements": ["aio_geojson_geonetnz_volcano==0.5"],
+ "requirements": ["aio_geojson_geonetnz_volcano==0.6"],
"codeowners": ["@exxamalte"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/geonetnz_volcano/translations/he.json b/homeassistant/components/geonetnz_volcano/translations/he.json
new file mode 100644
index 00000000000..59a1fbe0eed
--- /dev/null
+++ b/homeassistant/components/geonetnz_volcano/translations/he.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py
index e74cec8e151..00c4a526c46 100644
--- a/homeassistant/components/gios/air_quality.py
+++ b/homeassistant/components/gios/air_quality.py
@@ -80,7 +80,7 @@ class GiosAirQuality(CoordinatorEntity, AirQualityEntity):
@property
def air_quality_index(self) -> str | None:
"""Return the air quality index."""
- return cast(Optional[str], self.coordinator.data.get(API_AQI, {}).get("value"))
+ return cast(Optional[str], self.coordinator.data.get(API_AQI).get("value"))
@property
def particulate_matter_2_5(self) -> float | None:
@@ -141,7 +141,7 @@ class GiosAirQuality(CoordinatorEntity, AirQualityEntity):
if sensor in self.coordinator.data:
self._attrs[f"{SENSOR_MAP[sensor]}_index"] = self.coordinator.data[
sensor
- ]["index"]
+ ].get("index")
self._attrs[ATTR_STATION] = self.coordinator.gios.station_name
return self._attrs
diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json
index 3dfb2a168db..f13da0e3f33 100644
--- a/homeassistant/components/gios/manifest.json
+++ b/homeassistant/components/gios/manifest.json
@@ -3,7 +3,7 @@
"name": "GIO\u015a",
"documentation": "https://www.home-assistant.io/integrations/gios",
"codeowners": ["@bieniu"],
- "requirements": ["gios==1.0.1"],
+ "requirements": ["gios==1.0.2"],
"config_flow": true,
"quality_scale": "platinum",
"iot_class": "cloud_polling"
diff --git a/homeassistant/components/gios/translations/he.json b/homeassistant/components/gios/translations/he.json
new file mode 100644
index 00000000000..5bae816fc44
--- /dev/null
+++ b/homeassistant/components/gios/translations/he.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \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": {
+ "name": "\u05e9\u05dd"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/translations/he.json b/homeassistant/components/glances/translations/he.json
index 6f4191da70d..f5ba6464a4e 100644
--- a/homeassistant/components/glances/translations/he.json
+++ b/homeassistant/components/glances/translations/he.json
@@ -1,9 +1,21 @@
{
"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": {
- "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "name": "\u05e9\u05dd",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
}
}
}
diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py
index 575ff2ba350..cea47c967a8 100644
--- a/homeassistant/components/goalzero/config_flow.py
+++ b/homeassistant/components/goalzero/config_flow.py
@@ -18,7 +18,7 @@ from .const import DEFAULT_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
-DATA_SCHEMA = vol.Schema({"host": str, "name": str})
+DATA_SCHEMA = vol.Schema({vol.Required("host"): str, vol.Required("name"): str})
class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json
index 6b88f0a1209..6f6eb052589 100644
--- a/homeassistant/components/goalzero/translations/de.json
+++ b/homeassistant/components/goalzero/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Konto wurde bereits konfiguriert",
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
"invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse",
"unknown": "Unerwarteter Fehler"
},
@@ -11,12 +11,17 @@
"unknown": "Unerwarteter Fehler"
},
"step": {
+ "confirm_discovery": {
+ "description": "Eine DHCP-Reservierung auf Ihrem Router wird empfohlen. Wenn sie nicht eingerichtet ist, ist das Ger\u00e4t m\u00f6glicherweise nicht mehr verf\u00fcgbar, bis Home Assistant die neue IP-Adresse erkennt. Schlagen Sie im Benutzerhandbuch Ihres Routers nach.",
+ "title": "Goal Zero Yeti"
+ },
"user": {
"data": {
"host": "Host",
"name": "Name"
},
- "description": "Zun\u00e4chst musst du die Goal Zero App herunterladen: https://www.goalzero.com/product-features/yeti-app/\n\nFolge den Anweisungen, um deinen Yeti mit deinem Wifi-Netzwerk zu verbinden. Bekomme dann die Host-IP von deinem Router. DHCP muss in den Router-Einstellungen f\u00fcr das Ger\u00e4t richtig eingerichtet werden, um sicherzustellen, dass sich die Host-IP nicht \u00e4ndert. Schaue hierzu im Benutzerhandbuch deines Routers nach."
+ "description": "Zuerst musst du die Goal Zero App herunterladen: https://www.goalzero.com/product-features/yeti-app/ \n\nFolge den Anweisungen, um deinen Yeti mit deinem WLAN-Netzwerk zu verbinden. Eine DHCP-Reservierung auf deinem Router wird empfohlen. Wenn es nicht eingerichtet ist, ist das Ger\u00e4t m\u00f6glicherweise nicht verf\u00fcgbar, bis Home Assistant die neue IP-Adresse erkennt. Schlage dazu im Benutzerhandbuch deines Routers nach.",
+ "title": "Goal Zero Yeti"
}
}
}
diff --git a/homeassistant/components/goalzero/translations/es.json b/homeassistant/components/goalzero/translations/es.json
index 4a2c7eeca62..06ee47fd1ca 100644
--- a/homeassistant/components/goalzero/translations/es.json
+++ b/homeassistant/components/goalzero/translations/es.json
@@ -9,6 +9,10 @@
"unknown": "Error inesperado"
},
"step": {
+ "confirm_discovery": {
+ "description": "Se recomienda reservar el DHCP en el router. Si no se configura, el dispositivo puede dejar de estar disponible hasta que el Home Assistant detecte la nueva direcci\u00f3n ip. Consulte el manual de usuario de su router.",
+ "title": "Goal Zero Yeti"
+ },
"user": {
"data": {
"host": "Host",
diff --git a/homeassistant/components/goalzero/translations/he.json b/homeassistant/components/goalzero/translations/he.json
new file mode 100644
index 00000000000..f645dcab778
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/he.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "name": "\u05e9\u05dd"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/hu.json b/homeassistant/components/goalzero/translations/hu.json
index c876a55301f..ebc56c1bbe5 100644
--- a/homeassistant/components/goalzero/translations/hu.json
+++ b/homeassistant/components/goalzero/translations/hu.json
@@ -1,7 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
diff --git a/homeassistant/components/goalzero/translations/pl.json b/homeassistant/components/goalzero/translations/pl.json
index 4b06301953e..3aba221bc4a 100644
--- a/homeassistant/components/goalzero/translations/pl.json
+++ b/homeassistant/components/goalzero/translations/pl.json
@@ -1,7 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane"
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
@@ -9,12 +11,16 @@
"unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
+ "confirm_discovery": {
+ "description": "Zaleca si\u0119 rezerwacj\u0119 DHCP w ustawieniach routera. Je\u015bli tego nie ustawisz, urz\u0105dzenie mo\u017ce sta\u0107 si\u0119 niedost\u0119pne, do czasu a\u017c Home Assistant wykryje nowy adres IP. Post\u0119puj wg instrukcji obs\u0142ugi routera.",
+ "title": "Goal Zero Yeti"
+ },
"user": {
"data": {
"host": "Nazwa hosta lub adres IP",
"name": "Nazwa"
},
- "description": "Najpierw musisz pobra\u0107 aplikacj\u0119 Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nPost\u0119puj zgodnie z instrukcjami, aby pod\u0142\u0105czy\u0107 Yeti do sieci Wi-Fi. W ustawieniach routera nale\u017cy skonfigurowa\u0107 rezerwacj\u0119 adres\u00f3w DHCP, aby upewni\u0107 si\u0119, \u017ce adres IP hosta nie ulegnie zmianie. Post\u0119puj wg instrukcji obs\u0142ugi routera.",
+ "description": "Najpierw musisz pobra\u0107 aplikacj\u0119 Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nPost\u0119puj zgodnie z instrukcjami, aby pod\u0142\u0105czy\u0107 Yeti do sieci Wi-Fi. Zaleca si\u0119 rezerwacj\u0119 DHCP w ustawieniach routera. Je\u015bli tego nie ustawisz, urz\u0105dzenie mo\u017ce sta\u0107 si\u0119 niedost\u0119pne, do czasu a\u017c Home Assistant wykryje nowy adres IP. Post\u0119puj wg instrukcji obs\u0142ugi routera.",
"title": "Goal Zero Yeti"
}
}
diff --git a/homeassistant/components/gogogate2/translations/de.json b/homeassistant/components/gogogate2/translations/de.json
index 5c0173a99cf..1ccb678e5c2 100644
--- a/homeassistant/components/gogogate2/translations/de.json
+++ b/homeassistant/components/gogogate2/translations/de.json
@@ -7,6 +7,7 @@
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung"
},
+ "flow_title": "{device} ({ip_address})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/gogogate2/translations/he.json b/homeassistant/components/gogogate2/translations/he.json
new file mode 100644
index 00000000000..53c14104022
--- /dev/null
+++ b/homeassistant/components/gogogate2/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "flow_title": "{device} ({ip_address})",
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gogogate2/translations/hu.json b/homeassistant/components/gogogate2/translations/hu.json
index cdc76a4145a..641046d7745 100644
--- a/homeassistant/components/gogogate2/translations/hu.json
+++ b/homeassistant/components/gogogate2/translations/hu.json
@@ -7,6 +7,7 @@
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
+ "flow_title": "{device} ({ip_address})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py
index 3294ff54c2e..2e43e20f124 100644
--- a/homeassistant/components/google_assistant/const.py
+++ b/homeassistant/components/google_assistant/const.py
@@ -15,6 +15,7 @@ from homeassistant.components import (
media_player,
scene,
script,
+ select,
sensor,
switch,
vacuum,
@@ -39,6 +40,8 @@ CONF_PRIVATE_KEY = "private_key"
DEFAULT_EXPOSE_BY_DEFAULT = True
DEFAULT_EXPOSED_DOMAINS = [
+ "alarm_control_panel",
+ "binary_sensor",
"climate",
"cover",
"fan",
@@ -47,15 +50,14 @@ DEFAULT_EXPOSED_DOMAINS = [
"input_boolean",
"input_select",
"light",
+ "lock",
"media_player",
"scene",
"script",
+ "select",
+ "sensor",
"switch",
"vacuum",
- "lock",
- "binary_sensor",
- "sensor",
- "alarm_control_panel",
]
PREFIX_TYPES = "action.devices.types."
@@ -117,6 +119,7 @@ EVENT_QUERY_RECEIVED = "google_assistant_query"
EVENT_SYNC_RECEIVED = "google_assistant_sync"
DOMAIN_TO_GOOGLE_TYPES = {
+ alarm_control_panel.DOMAIN: TYPE_ALARM,
camera.DOMAIN: TYPE_CAMERA,
climate.DOMAIN: TYPE_THERMOSTAT,
cover.DOMAIN: TYPE_BLINDS,
@@ -130,9 +133,9 @@ DOMAIN_TO_GOOGLE_TYPES = {
media_player.DOMAIN: TYPE_SETTOP,
scene.DOMAIN: TYPE_SCENE,
script.DOMAIN: TYPE_SCENE,
+ select.DOMAIN: TYPE_SENSOR,
switch.DOMAIN: TYPE_SWITCH,
vacuum.DOMAIN: TYPE_VACUUM,
- alarm_control_panel.DOMAIN: TYPE_ALARM,
}
DEVICE_CLASS_TO_GOOGLE_TYPES = {
diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py
index f7c57732876..c3f8ba3bffd 100644
--- a/homeassistant/components/google_assistant/report_state.py
+++ b/homeassistant/components/google_assistant/report_state.py
@@ -106,7 +106,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig
"""Check if the serialized data has changed."""
return old_extra_arg != new_extra_arg
- async def inital_report(_now):
+ async def initial_report(_now):
"""Report initially all states."""
nonlocal unsub, checker
entities = {}
@@ -140,7 +140,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig
MATCH_ALL, async_entity_state_listener
)
- unsub = async_call_later(hass, INITIAL_REPORT_DELAY, inital_report)
+ unsub = async_call_later(hass, INITIAL_REPORT_DELAY, initial_report)
@callback
def unsub_all():
diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py
index 8286e527159..0c547f18741 100644
--- a/homeassistant/components/google_assistant/trait.py
+++ b/homeassistant/components/google_assistant/trait.py
@@ -17,6 +17,7 @@ from homeassistant.components import (
media_player,
scene,
script,
+ select,
sensor,
switch,
vacuum,
@@ -121,6 +122,7 @@ COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
COMMAND_THERMOSTAT_SET_MODE = f"{PREFIX_COMMANDS}ThermostatSetMode"
COMMAND_LOCKUNLOCK = f"{PREFIX_COMMANDS}LockUnlock"
COMMAND_FANSPEED = f"{PREFIX_COMMANDS}SetFanSpeed"
+COMMAND_FANSPEEDRELATIVE = f"{PREFIX_COMMANDS}SetFanSpeedRelative"
COMMAND_MODES = f"{PREFIX_COMMANDS}SetModes"
COMMAND_INPUT = f"{PREFIX_COMMANDS}SetInput"
COMMAND_NEXT_INPUT = f"{PREFIX_COMMANDS}NextInput"
@@ -1276,10 +1278,9 @@ class FanSpeedTrait(_Trait):
reversible = False
if domain == fan.DOMAIN:
+ # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
modes = self.state.attributes.get(fan.ATTR_SPEED_LIST, [])
for mode in modes:
- if mode not in self.speed_synonyms:
- continue
speed = {
"speed_name": mode,
"speed_values": [
@@ -1321,6 +1322,7 @@ class FanSpeedTrait(_Trait):
if speed is not None:
response["on"] = speed != fan.SPEED_OFF
response["currentFanSpeedSetting"] = speed
+ if percent is not None:
response["currentFanSpeedPercent"] = percent
return response
@@ -1369,6 +1371,7 @@ class ModesTrait(_Trait):
commands = [COMMAND_MODES]
SYNONYMS = {
+ "preset mode": ["preset mode", "mode", "preset"],
"sound mode": ["sound mode", "effects"],
"option": ["option", "setting", "mode", "value"],
}
@@ -1376,9 +1379,15 @@ class ModesTrait(_Trait):
@staticmethod
def supported(domain, features, device_class, _):
"""Test if state is supported."""
+ if domain == fan.DOMAIN and features & fan.SUPPORT_PRESET_MODE:
+ return True
+
if domain == input_select.DOMAIN:
return True
+ if domain == select.DOMAIN:
+ return True
+
if domain == humidifier.DOMAIN and features & humidifier.SUPPORT_MODES:
return True
@@ -1419,8 +1428,10 @@ class ModesTrait(_Trait):
modes = []
for domain, attr, name in (
+ (fan.DOMAIN, fan.ATTR_PRESET_MODES, "preset mode"),
(media_player.DOMAIN, media_player.ATTR_SOUND_MODE_LIST, "sound mode"),
(input_select.DOMAIN, input_select.ATTR_OPTIONS, "option"),
+ (select.DOMAIN, select.ATTR_OPTIONS, "option"),
(humidifier.DOMAIN, humidifier.ATTR_AVAILABLE_MODES, "mode"),
(light.DOMAIN, light.ATTR_EFFECT_LIST, "effect"),
):
@@ -1445,11 +1456,16 @@ class ModesTrait(_Trait):
response = {}
mode_settings = {}
- if self.state.domain == media_player.DOMAIN:
+ if self.state.domain == fan.DOMAIN:
+ if fan.ATTR_PRESET_MODES in attrs:
+ mode_settings["preset mode"] = attrs.get(fan.ATTR_PRESET_MODE)
+ elif self.state.domain == media_player.DOMAIN:
if media_player.ATTR_SOUND_MODE_LIST in attrs:
mode_settings["sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE)
elif self.state.domain == input_select.DOMAIN:
mode_settings["option"] = self.state.state
+ elif self.state.domain == select.DOMAIN:
+ mode_settings["option"] = self.state.state
elif self.state.domain == humidifier.DOMAIN:
if ATTR_MODE in attrs:
mode_settings["mode"] = attrs.get(ATTR_MODE)
@@ -1466,8 +1482,22 @@ class ModesTrait(_Trait):
"""Execute a SetModes command."""
settings = params.get("updateModeSettings")
+ if self.state.domain == fan.DOMAIN:
+ preset_mode = settings["preset mode"]
+ await self.hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_SET_PRESET_MODE,
+ {
+ ATTR_ENTITY_ID: self.state.entity_id,
+ fan.ATTR_PRESET_MODE: preset_mode,
+ },
+ blocking=True,
+ context=data.context,
+ )
+ return
+
if self.state.domain == input_select.DOMAIN:
- option = params["updateModeSettings"]["option"]
+ option = settings["option"]
await self.hass.services.async_call(
input_select.DOMAIN,
input_select.SERVICE_SELECT_OPTION,
@@ -1480,6 +1510,20 @@ class ModesTrait(_Trait):
)
return
+ if self.state.domain == select.DOMAIN:
+ option = settings["option"]
+ await self.hass.services.async_call(
+ select.DOMAIN,
+ select.SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: self.state.entity_id,
+ select.ATTR_OPTION: option,
+ },
+ blocking=True,
+ context=data.context,
+ )
+ return
+
if self.state.domain == humidifier.DOMAIN:
requested_mode = settings["mode"]
await self.hass.services.async_call(
@@ -1508,26 +1552,25 @@ class ModesTrait(_Trait):
)
return
- if self.state.domain != media_player.DOMAIN:
- _LOGGER.info(
- "Received an Options command for unrecognised domain %s",
- self.state.domain,
- )
- return
+ if self.state.domain == media_player.DOMAIN:
+ sound_mode = settings.get("sound mode")
+ if sound_mode:
+ await self.hass.services.async_call(
+ media_player.DOMAIN,
+ media_player.SERVICE_SELECT_SOUND_MODE,
+ {
+ ATTR_ENTITY_ID: self.state.entity_id,
+ media_player.ATTR_SOUND_MODE: sound_mode,
+ },
+ blocking=True,
+ context=data.context,
+ )
- sound_mode = settings.get("sound mode")
-
- if sound_mode:
- await self.hass.services.async_call(
- media_player.DOMAIN,
- media_player.SERVICE_SELECT_SOUND_MODE,
- {
- ATTR_ENTITY_ID: self.state.entity_id,
- media_player.ATTR_SOUND_MODE: sound_mode,
- },
- blocking=True,
- context=data.context,
- )
+ _LOGGER.info(
+ "Received an Options command for unrecognised domain %s",
+ self.state.domain,
+ )
+ return
@register_trait
diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json
index 890479f9ffd..b566f3447f4 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.2"],
+ "requirements": ["gTTS==2.2.3"],
"codeowners": [],
"iot_class": "cloud_push"
}
diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py
index c9a5eef2c83..9f6ca3a88b8 100644
--- a/homeassistant/components/google_translate/tts.py
+++ b/homeassistant/components/google_translate/tts.py
@@ -12,6 +12,7 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_LANGUAGES = [
"af",
"ar",
+ "bg",
"bn",
"bs",
"ca",
diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py
index bad6edd119e..88fe587fbd0 100644
--- a/homeassistant/components/google_travel_time/__init__.py
+++ b/homeassistant/components/google_travel_time/__init__.py
@@ -12,18 +12,16 @@ PLATFORMS = ["sensor"]
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Google Maps Travel Time from a config entry."""
- if config_entry.unique_id is not None:
- hass.config_entries.async_update_entry(config_entry, unique_id=None)
+ if entry.unique_id is not None:
+ hass.config_entries.async_update_entry(entry, unique_id=None)
ent_reg = async_get(hass)
- for entity in async_entries_for_config_entry(ent_reg, config_entry.entry_id):
- ent_reg.async_update_entity(
- entity.entity_id, new_unique_id=config_entry.entry_id
- )
+ for entity in async_entries_for_config_entry(ent_reg, entry.entry_id):
+ ent_reg.async_update_entity(entity.entity_id, new_unique_id=entry.entry_id)
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
diff --git a/homeassistant/components/google_travel_time/translations/he.json b/homeassistant/components/google_travel_time/translations/he.json
new file mode 100644
index 00000000000..0db92654b41
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/he.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \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": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "destination": "\u05d9\u05e2\u05d3",
+ "name": "\u05e9\u05dd",
+ "origin": "\u05de\u05e7\u05d5\u05e8"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "\u05e9\u05e4\u05d4",
+ "time": "\u05d6\u05de\u05df",
+ "units": "\u05d9\u05d7\u05d9\u05d3\u05d5\u05ea"
+ }
+ }
+ }
+ },
+ "title": "\u05d6\u05de\u05df \u05e0\u05e1\u05d9\u05e2\u05d4 \u05d1\u05d2\u05d5\u05d2\u05dc \u05de\u05e4\u05d5\u05ea"
+}
\ No newline at end of file
diff --git a/homeassistant/components/gpmdp/manifest.json b/homeassistant/components/gpmdp/manifest.json
index 2b65226b0c1..51fad8e9e71 100644
--- a/homeassistant/components/gpmdp/manifest.json
+++ b/homeassistant/components/gpmdp/manifest.json
@@ -1,6 +1,7 @@
{
"domain": "gpmdp",
"name": "Google Play Music Desktop Player (GPMDP)",
+ "disabled": "Integration has incompatible requirements.",
"documentation": "https://www.home-assistant.io/integrations/gpmdp",
"requirements": ["websocket-client==0.54.0"],
"dependencies": ["configurator"],
diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py
index 5bce10ab088..2493054473a 100644
--- a/homeassistant/components/gpslogger/device_tracker.py
+++ b/homeassistant/components/gpslogger/device_tracker.py
@@ -1,6 +1,7 @@
"""Support for the GPSLogger device tracking."""
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
from homeassistant.components.device_tracker.config_entry import TrackerEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_GPS_ACCURACY,
@@ -22,7 +23,9 @@ from .const import (
)
-async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities
+):
"""Configure a dispatcher connection based on a config entry."""
@callback
diff --git a/homeassistant/components/gpslogger/translations/he.json b/homeassistant/components/gpslogger/translations/he.json
new file mode 100644
index 00000000000..ebee9aee976
--- /dev/null
+++ b/homeassistant/components/gpslogger/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "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.",
+ "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py
index b873d5ba4d3..b91324ba4b3 100644
--- a/homeassistant/components/gree/__init__.py
+++ b/homeassistant/components/gree/__init__.py
@@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = [CLIMATE_DOMAIN, SWITCH_DOMAIN]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Gree Climate from a config entry."""
hass.data.setdefault(DOMAIN, {})
gree_discovery = DiscoveryService(hass)
diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py
index e468195ff92..acd57ef590d 100644
--- a/homeassistant/components/gree/climate.py
+++ b/homeassistant/components/gree/climate.py
@@ -4,6 +4,10 @@ from __future__ import annotations
import logging
from greeclimate.device import (
+ TEMP_MAX,
+ TEMP_MAX_F,
+ TEMP_MIN,
+ TEMP_MIN_F,
FanSpeed,
HorizontalSwing,
Mode,
@@ -55,8 +59,6 @@ from .const import (
DOMAIN,
FAN_MEDIUM_HIGH,
FAN_MEDIUM_LOW,
- MAX_TEMP,
- MIN_TEMP,
TARGET_TEMPERATURE_STEP,
)
@@ -184,12 +186,12 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity):
@property
def min_temp(self) -> float:
"""Return the minimum temperature supported by the device."""
- return MIN_TEMP
+ return TEMP_MIN if self.temperature_unit == TEMP_CELSIUS else TEMP_MIN_F
@property
def max_temp(self) -> float:
"""Return the maximum temperature supported by the device."""
- return MAX_TEMP
+ return TEMP_MAX if self.temperature_unit == TEMP_CELSIUS else TEMP_MAX_F
@property
def target_temperature_step(self) -> float:
diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py
index 2d9a48496b2..b4df7a1acde 100644
--- a/homeassistant/components/gree/const.py
+++ b/homeassistant/components/gree/const.py
@@ -16,9 +16,6 @@ COORDINATOR = "coordinator"
FAN_MEDIUM_LOW = "medium low"
FAN_MEDIUM_HIGH = "medium high"
-MIN_TEMP = 16
-MAX_TEMP = 30
-
MAX_ERRORS = 2
TARGET_TEMPERATURE_STEP = 1
diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json
index 58ddb62216b..8108df18cc8 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==0.11.4"],
+ "requirements": ["greeclimate==0.11.7"],
"codeowners": ["@cmroche"],
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/gree/translations/he.json b/homeassistant/components/gree/translations/he.json
new file mode 100644
index 00000000000..d3d68dccc93
--- /dev/null
+++ b/homeassistant/components/gree/translations/he.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\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": {
+ "confirm": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py
index 4e792bf56e4..337a471eb2b 100644
--- a/homeassistant/components/greeneye_monitor/sensor.py
+++ b/homeassistant/components/greeneye_monitor/sensor.py
@@ -88,6 +88,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class GEMSensor(SensorEntity):
"""Base class for GreenEye Monitor sensors."""
+ _attr_should_poll = False
+
def __init__(self, monitor_serial_number, name, sensor_type, number):
"""Construct the entity."""
self._monitor_serial_number = monitor_serial_number
@@ -96,11 +98,6 @@ class GEMSensor(SensorEntity):
self._sensor_type = sensor_type
self._number = number
- @property
- def should_poll(self):
- """GEM pushes changes, so this returns False."""
- return False
-
@property
def unique_id(self):
"""Return a unique ID for this sensor."""
@@ -148,6 +145,9 @@ class GEMSensor(SensorEntity):
class CurrentSensor(GEMSensor):
"""Entity showing power usage on one channel of the monitor."""
+ _attr_icon = CURRENT_SENSOR_ICON
+ _attr_unit_of_measurement = UNIT_WATTS
+
def __init__(self, monitor_serial_number, number, name, net_metering):
"""Construct the entity."""
super().__init__(monitor_serial_number, name, "current", number)
@@ -156,16 +156,6 @@ class CurrentSensor(GEMSensor):
def _get_sensor(self, monitor):
return monitor.channels[self._number - 1]
- @property
- def icon(self):
- """Return the icon that should represent this sensor in the UI."""
- return CURRENT_SENSOR_ICON
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement used by this sensor."""
- return UNIT_WATTS
-
@property
def state(self):
"""Return the current number of watts being used by the channel."""
@@ -191,6 +181,8 @@ class CurrentSensor(GEMSensor):
class PulseCounter(GEMSensor):
"""Entity showing rate of change in one pulse counter of the monitor."""
+ _attr_icon = COUNTER_ICON
+
def __init__(
self,
monitor_serial_number,
@@ -209,11 +201,6 @@ class PulseCounter(GEMSensor):
def _get_sensor(self, monitor):
return monitor.pulse_counters[self._number - 1]
- @property
- def icon(self):
- """Return the icon that should represent this sensor in the UI."""
- return COUNTER_ICON
-
@property
def state(self):
"""Return the current rate of change for the given pulse counter."""
@@ -253,6 +240,8 @@ class PulseCounter(GEMSensor):
class TemperatureSensor(GEMSensor):
"""Entity showing temperature from one temperature sensor."""
+ _attr_icon = TEMPERATURE_ICON
+
def __init__(self, monitor_serial_number, number, name, unit):
"""Construct the entity."""
super().__init__(monitor_serial_number, name, "temp", number)
@@ -261,11 +250,6 @@ class TemperatureSensor(GEMSensor):
def _get_sensor(self, monitor):
return monitor.temperature_sensors[self._number - 1]
- @property
- def icon(self):
- """Return the icon that should represent this sensor in the UI."""
- return TEMPERATURE_ICON
-
@property
def state(self):
"""Return the current temperature being reported by this sensor."""
@@ -283,6 +267,9 @@ class TemperatureSensor(GEMSensor):
class VoltageSensor(GEMSensor):
"""Entity showing voltage."""
+ _attr_icon = VOLTAGE_ICON
+ _attr_unit_of_measurement = VOLT
+
def __init__(self, monitor_serial_number, number, name):
"""Construct the entity."""
super().__init__(monitor_serial_number, name, "volts", number)
@@ -291,11 +278,6 @@ class VoltageSensor(GEMSensor):
"""Wire the updates to the monitor itself, since there is no voltage element in the API."""
return monitor
- @property
- def icon(self):
- """Return the icon that should represent this sensor in the UI."""
- return VOLTAGE_ICON
-
@property
def state(self):
"""Return the current voltage being reported by this sensor."""
@@ -303,8 +285,3 @@ class VoltageSensor(GEMSensor):
return None
return self._sensor.voltage
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement for this sensor."""
- return VOLT
diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py
index 5e8d18b28e2..b5022582d9e 100644
--- a/homeassistant/components/group/cover.py
+++ b/homeassistant/components/group/cover.py
@@ -1,6 +1,8 @@
"""This platform allows several cover to be grouped into one cover."""
from __future__ import annotations
+from typing import Any
+
import voluptuous as vol
from homeassistant.components.cover import (
@@ -38,15 +40,14 @@ from homeassistant.const import (
STATE_OPEN,
STATE_OPENING,
)
-from homeassistant.core import CoreState, State
+from homeassistant.core import CoreState, Event, HomeAssistant, State
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
+from homeassistant.helpers.typing import ConfigType
from . import GroupEntity
-# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
-# mypy: no-check-untyped-defs
-
KEY_OPEN_CLOSE = "open_close"
KEY_STOP = "stop"
KEY_POSITION = "position"
@@ -62,7 +63,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: AddEntitiesCallback,
+ discovery_info: dict[str, Any] | None = None,
+) -> None:
"""Set up the Group Cover platform."""
async_add_entities([CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])])
@@ -70,17 +76,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class CoverGroup(GroupEntity, CoverEntity):
"""Representation of a CoverGroup."""
- def __init__(self, name, entities):
- """Initialize a CoverGroup entity."""
- self._name = name
- self._is_closed = False
- self._is_closing = False
- self._is_opening = False
- self._cover_position: int | None = 100
- self._tilt_position = None
- self._supported_features = 0
- self._assumed_state = True
+ _attr_is_closed: bool | None = False
+ _attr_is_opening: bool | None = False
+ _attr_is_closing: bool | None = False
+ _attr_current_cover_position: int | None = 100
+ _attr_assumed_state: bool = True
+ def __init__(self, name: str, entities: list[str]) -> None:
+ """Initialize a CoverGroup entity."""
self._entities = entities
self._covers: dict[str, set[str]] = {
KEY_OPEN_CLOSE: set(),
@@ -93,11 +96,16 @@ class CoverGroup(GroupEntity, CoverEntity):
KEY_POSITION: set(),
}
- async def _update_supported_features_event(self, event):
+ self._attr_name = name
+ self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities}
+
+ async def _update_supported_features_event(self, event: Event) -> None:
self.async_set_context(event.context)
- await self.async_update_supported_features(
- event.data.get("entity_id"), event.data.get("new_state")
- )
+ entity = event.data.get("entity_id")
+ if entity is not None:
+ await self.async_update_supported_features(
+ entity, event.data.get("new_state")
+ )
async def async_update_supported_features(
self,
@@ -146,7 +154,7 @@ class CoverGroup(GroupEntity, CoverEntity):
if update_state:
await self.async_defer_or_update_ha_state()
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Register listeners."""
for entity_id in self._entities:
new_state = self.hass.states.get(entity_id)
@@ -166,73 +174,28 @@ class CoverGroup(GroupEntity, CoverEntity):
return
await super().async_added_to_hass()
- @property
- def name(self):
- """Return the name of the cover."""
- return self._name
-
- @property
- def assumed_state(self):
- """Enable buttons even if at end position."""
- return self._assumed_state
-
- @property
- def supported_features(self):
- """Flag supported features for the cover."""
- return self._supported_features
-
- @property
- def is_closed(self):
- """Return if all covers in group are closed."""
- return self._is_closed
-
- @property
- def is_opening(self):
- """Return if the cover is opening or not."""
- return self._is_opening
-
- @property
- def is_closing(self):
- """Return if the cover is closing or not."""
- return self._is_closing
-
- @property
- def current_cover_position(self) -> int | None:
- """Return current position for all covers."""
- return self._cover_position
-
- @property
- def current_cover_tilt_position(self):
- """Return current tilt position for all covers."""
- return self._tilt_position
-
- @property
- def extra_state_attributes(self):
- """Return the state attributes for the cover group."""
- return {ATTR_ENTITY_ID: self._entities}
-
- async def async_open_cover(self, **kwargs):
+ async def async_open_cover(self, **kwargs: Any) -> None:
"""Move the covers up."""
data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
DOMAIN, SERVICE_OPEN_COVER, data, blocking=True, context=self._context
)
- async def async_close_cover(self, **kwargs):
+ async def async_close_cover(self, **kwargs: Any) -> None:
"""Move the covers down."""
data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True, context=self._context
)
- async def async_stop_cover(self, **kwargs):
+ async def async_stop_cover(self, **kwargs: Any) -> None:
"""Fire the stop action."""
data = {ATTR_ENTITY_ID: self._covers[KEY_STOP]}
await self.hass.services.async_call(
DOMAIN, SERVICE_STOP_COVER, data, blocking=True, context=self._context
)
- async def async_set_cover_position(self, **kwargs):
+ async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Set covers position."""
data = {
ATTR_ENTITY_ID: self._covers[KEY_POSITION],
@@ -246,28 +209,28 @@ class CoverGroup(GroupEntity, CoverEntity):
context=self._context,
)
- async def async_open_cover_tilt(self, **kwargs):
+ async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Tilt covers open."""
data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True, context=self._context
)
- async def async_close_cover_tilt(self, **kwargs):
+ async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Tilt covers closed."""
data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True, context=self._context
)
- async def async_stop_cover_tilt(self, **kwargs):
+ async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
"""Stop cover tilt."""
data = {ATTR_ENTITY_ID: self._tilts[KEY_STOP]}
await self.hass.services.async_call(
DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True, context=self._context
)
- async def async_set_cover_tilt_position(self, **kwargs):
+ async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Set tilt position."""
data = {
ATTR_ENTITY_ID: self._tilts[KEY_POSITION],
@@ -281,31 +244,31 @@ class CoverGroup(GroupEntity, CoverEntity):
context=self._context,
)
- async def async_update(self):
+ async def async_update(self) -> None:
"""Update state and attributes."""
- self._assumed_state = False
+ self._attr_assumed_state = False
- self._is_closed = True
- self._is_closing = False
- self._is_opening = False
+ self._attr_is_closed = True
+ self._attr_is_closing = False
+ self._attr_is_opening = False
for entity_id in self._entities:
state = self.hass.states.get(entity_id)
if not state:
continue
if state.state == STATE_OPEN:
- self._is_closed = False
+ self._attr_is_closed = False
break
if state.state == STATE_CLOSING:
- self._is_closing = True
+ self._attr_is_closing = True
break
if state.state == STATE_OPENING:
- self._is_opening = True
+ self._attr_is_opening = True
break
- self._cover_position = None
+ self._attr_current_cover_position = None
if self._covers[KEY_POSITION]:
- position = -1
- self._cover_position = 0 if self.is_closed else 100
+ position: int | None = -1
+ self._attr_current_cover_position = 0 if self.is_closed else 100
for entity_id in self._covers[KEY_POSITION]:
state = self.hass.states.get(entity_id)
if state is None:
@@ -314,16 +277,16 @@ class CoverGroup(GroupEntity, CoverEntity):
if position == -1:
position = pos
elif position != pos:
- self._assumed_state = True
+ self._attr_assumed_state = True
break
else:
if position != -1:
- self._cover_position = position
+ self._attr_current_cover_position = position
- self._tilt_position = None
+ self._attr_current_cover_tilt_position = None
if self._tilts[KEY_POSITION]:
position = -1
- self._tilt_position = 100
+ self._attr_current_cover_tilt_position = 100
for entity_id in self._tilts[KEY_POSITION]:
state = self.hass.states.get(entity_id)
if state is None:
@@ -332,11 +295,11 @@ class CoverGroup(GroupEntity, CoverEntity):
if position == -1:
position = pos
elif position != pos:
- self._assumed_state = True
+ self._attr_assumed_state = True
break
else:
if position != -1:
- self._tilt_position = position
+ self._attr_current_cover_tilt_position = position
supported_features = 0
supported_features |= (
@@ -351,13 +314,13 @@ class CoverGroup(GroupEntity, CoverEntity):
supported_features |= (
SUPPORT_SET_TILT_POSITION if self._tilts[KEY_POSITION] else 0
)
- self._supported_features = supported_features
+ self._attr_supported_features = supported_features
- if not self._assumed_state:
+ if not self._attr_assumed_state:
for entity_id in self._entities:
state = self.hass.states.get(entity_id)
if state is None:
continue
if state and state.attributes.get(ATTR_ASSUMED_STATE):
- self._assumed_state = True
+ self._attr_assumed_state = True
break
diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py
index 9567735c9eb..3f5a6eaf13e 100644
--- a/homeassistant/components/group/light.py
+++ b/homeassistant/components/group/light.py
@@ -1,11 +1,10 @@
"""This platform allows several lights to be grouped into one light."""
from __future__ import annotations
-import asyncio
from collections import Counter
from collections.abc import Iterator
import itertools
-from typing import Any, Callable, cast
+from typing import Any, Callable, Set, cast
import voluptuous as vol
@@ -34,8 +33,6 @@ from homeassistant.components.light import (
SUPPORT_FLASH,
SUPPORT_TRANSITION,
SUPPORT_WHITE_VALUE,
- color_supported,
- color_temp_supported,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -45,17 +42,14 @@ from homeassistant.const import (
STATE_ON,
STATE_UNAVAILABLE,
)
-from homeassistant.core import CoreState, HomeAssistant, State
+from homeassistant.core import CoreState, Event, HomeAssistant, State
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.typing import ConfigType
-from homeassistant.util import color as color_util
from . import GroupEntity
-# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
-# mypy: no-check-untyped-defs
-
DEFAULT_NAME = "Light Group"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
@@ -71,7 +65,10 @@ SUPPORT_GROUP_LIGHT = (
async def async_setup_platform(
- hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: AddEntitiesCallback,
+ discovery_info: dict[str, Any] | None = None,
) -> None:
"""Initialize light.group platform."""
async_add_entities(
@@ -82,33 +79,25 @@ async def async_setup_platform(
class LightGroup(GroupEntity, light.LightEntity):
"""Representation of a light group."""
+ _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, name: str, entity_ids: list[str]) -> None:
"""Initialize a light group."""
- self._name = name
self._entity_ids = entity_ids
- self._is_on = False
- self._available = False
- self._icon = "mdi:lightbulb-group"
- self._brightness: int | None = None
- self._color_mode: str | None = None
- self._hs_color: tuple[float, float] | None = None
- self._rgb_color: tuple[int, int, int] | None = None
- self._rgbw_color: tuple[int, int, int, int] | None = None
- self._rgbww_color: tuple[int, int, int, int, int] | None = None
- self._xy_color: tuple[float, float] | None = None
- self._color_temp: int | None = None
- self._min_mireds: int = 154
- self._max_mireds: int = 500
self._white_value: int | None = None
- self._effect_list: list[str] | None = None
- self._effect: str | None = None
- self._supported_color_modes: set[str] | None = None
- self._supported_features: int = 0
+
+ self._attr_name = name
+ self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
- async def async_state_changed_listener(event):
+ async def async_state_changed_listener(event: Event) -> None:
"""Handle child updates."""
self.async_set_context(event.context)
await self.async_defer_or_update_ha_state()
@@ -125,115 +114,14 @@ class LightGroup(GroupEntity, light.LightEntity):
await super().async_added_to_hass()
- @property
- def name(self) -> str:
- """Return the name of the entity."""
- return self._name
-
- @property
- def is_on(self) -> bool:
- """Return the on/off state of the light group."""
- return self._is_on
-
- @property
- def available(self) -> bool:
- """Return whether the light group is available."""
- return self._available
-
- @property
- def icon(self):
- """Return the light group icon."""
- return self._icon
-
- @property
- def brightness(self) -> int | None:
- """Return the brightness of this light group between 0..255."""
- return self._brightness
-
- @property
- def color_mode(self) -> str | None:
- """Return the color mode of the light."""
- return self._color_mode
-
- @property
- def hs_color(self) -> tuple[float, float] | None:
- """Return the HS color value [float, float]."""
- return self._hs_color
-
- @property
- def rgb_color(self) -> tuple[int, int, int] | None:
- """Return the rgb color value [int, int, int]."""
- return self._rgb_color
-
- @property
- def rgbw_color(self) -> tuple[int, int, int, int] | None:
- """Return the rgbw color value [int, int, int, int]."""
- return self._rgbw_color
-
- @property
- def rgbww_color(self) -> tuple[int, int, int, int, int] | None:
- """Return the rgbww color value [int, int, int, int, int]."""
- return self._rgbww_color
-
- @property
- def xy_color(self) -> tuple[float, float] | None:
- """Return the xy color value [float, float]."""
- return self._xy_color
-
- @property
- def color_temp(self) -> int | None:
- """Return the CT color value in mireds."""
- return self._color_temp
-
- @property
- def min_mireds(self) -> int:
- """Return the coldest color_temp that this light group supports."""
- return self._min_mireds
-
- @property
- def max_mireds(self) -> int:
- """Return the warmest color_temp that this light group supports."""
- return self._max_mireds
-
@property
def white_value(self) -> int | None:
"""Return the white value of this light group between 0..255."""
return self._white_value
- @property
- def effect_list(self) -> list[str] | None:
- """Return the list of supported effects."""
- return self._effect_list
-
- @property
- def effect(self) -> str | None:
- """Return the current effect."""
- return self._effect
-
- @property
- def supported_color_modes(self) -> set | None:
- """Flag supported color modes."""
- return self._supported_color_modes
-
- @property
- def supported_features(self) -> int:
- """Flag supported features."""
- return self._supported_features
-
- @property
- def should_poll(self) -> bool:
- """No polling needed for a light group."""
- return False
-
- @property
- def extra_state_attributes(self):
- """Return the state attributes for the light group."""
- return {ATTR_ENTITY_ID: self._entity_ids}
-
- async def async_turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs: Any) -> None:
"""Forward the turn_on command to all lights in the light group."""
data = {ATTR_ENTITY_ID: self._entity_ids}
- emulate_color_temp_entity_ids = []
if ATTR_BRIGHTNESS in kwargs:
data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS]
@@ -256,21 +144,6 @@ class LightGroup(GroupEntity, light.LightEntity):
if ATTR_COLOR_TEMP in kwargs:
data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP]
- # Create a new entity list to mutate
- updated_entities = list(self._entity_ids)
-
- # Walk through initial entity ids, split entity lists by support
- for entity_id in self._entity_ids:
- state = self.hass.states.get(entity_id)
- if not state:
- continue
- support = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES)
- # Only pass color temperature to supported entity_ids
- if color_supported(support) and not color_temp_supported(support):
- emulate_color_temp_entity_ids.append(entity_id)
- updated_entities.remove(entity_id)
- data[ATTR_ENTITY_ID] = updated_entities
-
if ATTR_WHITE_VALUE in kwargs:
data[ATTR_WHITE_VALUE] = kwargs[ATTR_WHITE_VALUE]
@@ -283,44 +156,15 @@ class LightGroup(GroupEntity, light.LightEntity):
if ATTR_FLASH in kwargs:
data[ATTR_FLASH] = kwargs[ATTR_FLASH]
- if not emulate_color_temp_entity_ids:
- await self.hass.services.async_call(
- light.DOMAIN,
- light.SERVICE_TURN_ON,
- data,
- blocking=True,
- context=self._context,
- )
- return
-
- emulate_color_temp_data = data.copy()
- temp_k = color_util.color_temperature_mired_to_kelvin(
- emulate_color_temp_data[ATTR_COLOR_TEMP]
- )
- hs_color = color_util.color_temperature_to_hs(temp_k)
- emulate_color_temp_data[ATTR_HS_COLOR] = hs_color
- del emulate_color_temp_data[ATTR_COLOR_TEMP]
-
- emulate_color_temp_data[ATTR_ENTITY_ID] = emulate_color_temp_entity_ids
-
- await asyncio.gather(
- self.hass.services.async_call(
- light.DOMAIN,
- light.SERVICE_TURN_ON,
- data,
- blocking=True,
- context=self._context,
- ),
- self.hass.services.async_call(
- light.DOMAIN,
- light.SERVICE_TURN_ON,
- emulate_color_temp_data,
- blocking=True,
- context=self._context,
- ),
+ await self.hass.services.async_call(
+ light.DOMAIN,
+ light.SERVICE_TURN_ON,
+ data,
+ blocking=True,
+ context=self._context,
)
- async def async_turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs: Any) -> None:
"""Forward the turn_off command to all lights in the light group."""
data = {ATTR_ENTITY_ID: self._entity_ids}
@@ -335,57 +179,60 @@ class LightGroup(GroupEntity, light.LightEntity):
context=self._context,
)
- async def async_update(self):
+ async def async_update(self) -> None:
"""Query all members and determine the light group state."""
all_states = [self.hass.states.get(x) for x in self._entity_ids]
states: list[State] = list(filter(None, all_states))
on_states = [state for state in states if state.state == STATE_ON]
- self._is_on = len(on_states) > 0
- self._available = any(state.state != STATE_UNAVAILABLE for state in states)
+ self._attr_is_on = len(on_states) > 0
+ self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states)
+ self._attr_brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS)
- self._brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS)
-
- self._hs_color = _reduce_attribute(on_states, ATTR_HS_COLOR, reduce=_mean_tuple)
- self._rgb_color = _reduce_attribute(
+ self._attr_hs_color = _reduce_attribute(
+ on_states, ATTR_HS_COLOR, reduce=_mean_tuple
+ )
+ self._attr_rgb_color = _reduce_attribute(
on_states, ATTR_RGB_COLOR, reduce=_mean_tuple
)
- self._rgbw_color = _reduce_attribute(
+ self._attr_rgbw_color = _reduce_attribute(
on_states, ATTR_RGBW_COLOR, reduce=_mean_tuple
)
- self._rgbww_color = _reduce_attribute(
+ self._attr_rgbww_color = _reduce_attribute(
on_states, ATTR_RGBWW_COLOR, reduce=_mean_tuple
)
- self._xy_color = _reduce_attribute(on_states, ATTR_XY_COLOR, reduce=_mean_tuple)
+ self._attr_xy_color = _reduce_attribute(
+ on_states, ATTR_XY_COLOR, reduce=_mean_tuple
+ )
self._white_value = _reduce_attribute(on_states, ATTR_WHITE_VALUE)
- self._color_temp = _reduce_attribute(on_states, ATTR_COLOR_TEMP)
- self._min_mireds = _reduce_attribute(
+ self._attr_color_temp = _reduce_attribute(on_states, ATTR_COLOR_TEMP)
+ self._attr_min_mireds = _reduce_attribute(
states, ATTR_MIN_MIREDS, default=154, reduce=min
)
- self._max_mireds = _reduce_attribute(
+ self._attr_max_mireds = _reduce_attribute(
states, ATTR_MAX_MIREDS, default=500, reduce=max
)
- self._effect_list = None
+ self._attr_effect_list = None
all_effect_lists = list(_find_state_attributes(states, ATTR_EFFECT_LIST))
if all_effect_lists:
# Merge all effects from all effect_lists with a union merge.
- self._effect_list = list(set().union(*all_effect_lists))
- self._effect_list.sort()
- if "None" in self._effect_list:
- self._effect_list.remove("None")
- self._effect_list.insert(0, "None")
+ self._attr_effect_list = list(set().union(*all_effect_lists))
+ self._attr_effect_list.sort()
+ if "None" in self._attr_effect_list:
+ self._attr_effect_list.remove("None")
+ self._attr_effect_list.insert(0, "None")
- self._effect = None
+ self._attr_effect = None
all_effects = list(_find_state_attributes(on_states, ATTR_EFFECT))
if all_effects:
# Report the most common effect.
effects_count = Counter(itertools.chain(all_effects))
- self._effect = effects_count.most_common(1)[0][0]
+ self._attr_effect = effects_count.most_common(1)[0][0]
- self._color_mode = None
+ self._attr_color_mode = None
all_color_modes = list(_find_state_attributes(on_states, ATTR_COLOR_MODE))
if all_color_modes:
# Report the most common color mode, select brightness and onoff last
@@ -394,24 +241,26 @@ class LightGroup(GroupEntity, light.LightEntity):
color_mode_count[COLOR_MODE_ONOFF] = -1
if COLOR_MODE_BRIGHTNESS in color_mode_count:
color_mode_count[COLOR_MODE_BRIGHTNESS] = 0
- self._color_mode = color_mode_count.most_common(1)[0][0]
+ self._attr_color_mode = color_mode_count.most_common(1)[0][0]
- self._supported_color_modes = None
+ self._attr_supported_color_modes = None
all_supported_color_modes = list(
_find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES)
)
if all_supported_color_modes:
# Merge all color modes.
- self._supported_color_modes = set().union(*all_supported_color_modes)
+ self._attr_supported_color_modes = cast(
+ Set[str], set().union(*all_supported_color_modes)
+ )
- self._supported_features = 0
+ self._attr_supported_features = 0
for support in _find_state_attributes(states, ATTR_SUPPORTED_FEATURES):
# Merge supported features by emulating support for every feature
# we find.
- self._supported_features |= support
+ self._attr_supported_features |= support
# Bitwise-and the supported features with the GroupedLight's features
# so that we don't break in the future when a new feature is added.
- self._supported_features &= SUPPORT_GROUP_LIGHT
+ self._attr_supported_features &= SUPPORT_GROUP_LIGHT
def _find_state_attributes(states: list[State], key: str) -> Iterator[Any]:
@@ -422,12 +271,12 @@ def _find_state_attributes(states: list[State], key: str) -> Iterator[Any]:
yield value
-def _mean_int(*args):
+def _mean_int(*args: Any) -> int:
"""Return the mean of the supplied values."""
return int(sum(args) / len(args))
-def _mean_tuple(*args):
+def _mean_tuple(*args: Any) -> tuple[float | Any, ...]:
"""Return the mean values along the columns of the supplied values."""
return tuple(sum(x) / len(x) for x in zip(*args))
diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py
new file mode 100644
index 00000000000..568812fd6e0
--- /dev/null
+++ b/homeassistant/components/group/media_player.py
@@ -0,0 +1,411 @@
+"""This platform allows several media players to be grouped into one media player."""
+from __future__ import annotations
+
+from typing import Any, Callable
+
+import voluptuous as vol
+
+from homeassistant.components.media_player import (
+ ATTR_MEDIA_CONTENT_ID,
+ ATTR_MEDIA_CONTENT_TYPE,
+ ATTR_MEDIA_SEEK_POSITION,
+ ATTR_MEDIA_SHUFFLE,
+ ATTR_MEDIA_VOLUME_LEVEL,
+ ATTR_MEDIA_VOLUME_MUTED,
+ DOMAIN,
+ PLATFORM_SCHEMA,
+ SERVICE_CLEAR_PLAYLIST,
+ SERVICE_MEDIA_NEXT_TRACK,
+ SERVICE_MEDIA_PAUSE,
+ SERVICE_MEDIA_PLAY,
+ SERVICE_MEDIA_PREVIOUS_TRACK,
+ SERVICE_MEDIA_SEEK,
+ SERVICE_MEDIA_STOP,
+ SERVICE_PLAY_MEDIA,
+ SERVICE_SHUFFLE_SET,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ SERVICE_VOLUME_MUTE,
+ SERVICE_VOLUME_SET,
+ SUPPORT_CLEAR_PLAYLIST,
+ SUPPORT_NEXT_TRACK,
+ SUPPORT_PAUSE,
+ SUPPORT_PLAY,
+ SUPPORT_PLAY_MEDIA,
+ SUPPORT_PREVIOUS_TRACK,
+ SUPPORT_SEEK,
+ SUPPORT_SHUFFLE_SET,
+ SUPPORT_STOP,
+ SUPPORT_TURN_OFF,
+ SUPPORT_TURN_ON,
+ SUPPORT_VOLUME_MUTE,
+ SUPPORT_VOLUME_SET,
+ SUPPORT_VOLUME_STEP,
+ MediaPlayerEntity,
+)
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
+ CONF_ENTITIES,
+ CONF_NAME,
+ STATE_OFF,
+ STATE_ON,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+)
+from homeassistant.core import HomeAssistant, State, callback
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.event import async_track_state_change_event
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType
+
+KEY_CLEAR_PLAYLIST = "clear_playlist"
+KEY_ON_OFF = "on_off"
+KEY_PAUSE_PLAY_STOP = "play"
+KEY_PLAY_MEDIA = "play_media"
+KEY_SHUFFLE = "shuffle"
+KEY_SEEK = "seek"
+KEY_TRACKS = "tracks"
+KEY_VOLUME = "volume"
+
+DEFAULT_NAME = "Media Group"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN),
+ }
+)
+
+
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: Callable,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
+ """Set up the Media Group platform."""
+ async_add_entities([MediaGroup(config[CONF_NAME], config[CONF_ENTITIES])])
+
+
+class MediaGroup(MediaPlayerEntity):
+ """Representation of a Media Group."""
+
+ def __init__(self, name: str, entities: list[str]) -> None:
+ """Initialize a Media Group entity."""
+ self._name = name
+ self._state: str | None = None
+ self._supported_features: int = 0
+
+ self._entities = entities
+ self._features: dict[str, set[str]] = {
+ KEY_CLEAR_PLAYLIST: set(),
+ KEY_ON_OFF: set(),
+ KEY_PAUSE_PLAY_STOP: set(),
+ KEY_PLAY_MEDIA: set(),
+ KEY_SHUFFLE: set(),
+ KEY_SEEK: set(),
+ KEY_TRACKS: set(),
+ KEY_VOLUME: set(),
+ }
+
+ @callback
+ def async_on_state_change(self, event: EventType) -> None:
+ """Update supported features and state when a new state is received."""
+ self.async_set_context(event.context)
+ self.async_update_supported_features(
+ event.data.get("entity_id"), event.data.get("new_state") # type: ignore
+ )
+ self.async_update_state()
+
+ @callback
+ def async_update_supported_features(
+ self,
+ entity_id: str,
+ new_state: State | None,
+ ) -> None:
+ """Update dictionaries with supported features."""
+ if not new_state:
+ for players in self._features.values():
+ players.discard(entity_id)
+ return
+
+ new_features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+ if new_features & SUPPORT_CLEAR_PLAYLIST:
+ self._features[KEY_CLEAR_PLAYLIST].add(entity_id)
+ else:
+ self._features[KEY_CLEAR_PLAYLIST].discard(entity_id)
+ if new_features & (SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK):
+ self._features[KEY_TRACKS].add(entity_id)
+ else:
+ self._features[KEY_TRACKS].discard(entity_id)
+ if new_features & (SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP):
+ self._features[KEY_PAUSE_PLAY_STOP].add(entity_id)
+ else:
+ self._features[KEY_PAUSE_PLAY_STOP].discard(entity_id)
+ if new_features & SUPPORT_PLAY_MEDIA:
+ self._features[KEY_PLAY_MEDIA].add(entity_id)
+ else:
+ self._features[KEY_PLAY_MEDIA].discard(entity_id)
+ if new_features & SUPPORT_SEEK:
+ self._features[KEY_SEEK].add(entity_id)
+ else:
+ self._features[KEY_SEEK].discard(entity_id)
+ if new_features & SUPPORT_SHUFFLE_SET:
+ self._features[KEY_SHUFFLE].add(entity_id)
+ else:
+ self._features[KEY_SHUFFLE].discard(entity_id)
+ if new_features & (SUPPORT_TURN_ON | SUPPORT_TURN_OFF):
+ self._features[KEY_ON_OFF].add(entity_id)
+ else:
+ self._features[KEY_ON_OFF].discard(entity_id)
+ if new_features & (
+ SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP
+ ):
+ self._features[KEY_VOLUME].add(entity_id)
+ else:
+ self._features[KEY_VOLUME].discard(entity_id)
+
+ async def async_added_to_hass(self) -> None:
+ """Register listeners."""
+ for entity_id in self._entities:
+ new_state = self.hass.states.get(entity_id)
+ self.async_update_supported_features(entity_id, new_state)
+ async_track_state_change_event(
+ self.hass, self._entities, self.async_on_state_change
+ )
+ self.async_update_state()
+
+ @property
+ def name(self) -> str:
+ """Return the name of the entity."""
+ return self._name
+
+ @property
+ def state(self) -> str | None:
+ """Return the state of the media group."""
+ return self._state
+
+ @property
+ def supported_features(self) -> int:
+ """Flag supported features."""
+ return self._supported_features
+
+ @property
+ def should_poll(self) -> bool:
+ """No polling needed for a media group."""
+ return False
+
+ @property
+ def device_state_attributes(self) -> dict:
+ """Return the state attributes for the media group."""
+ return {ATTR_ENTITY_ID: self._entities}
+
+ async def async_clear_playlist(self) -> None:
+ """Clear players playlist."""
+ data = {ATTR_ENTITY_ID: self._features[KEY_CLEAR_PLAYLIST]}
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_CLEAR_PLAYLIST,
+ data,
+ context=self._context,
+ )
+
+ async def async_media_next_track(self) -> None:
+ """Send next track command."""
+ data = {ATTR_ENTITY_ID: self._features[KEY_TRACKS]}
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_MEDIA_NEXT_TRACK,
+ data,
+ context=self._context,
+ )
+
+ async def async_media_pause(self) -> None:
+ """Send pause command."""
+ data = {ATTR_ENTITY_ID: self._features[KEY_PAUSE_PLAY_STOP]}
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_MEDIA_PAUSE,
+ data,
+ context=self._context,
+ )
+
+ async def async_media_play(self) -> None:
+ """Send play command."""
+ data = {ATTR_ENTITY_ID: self._features[KEY_PAUSE_PLAY_STOP]}
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_MEDIA_PLAY,
+ data,
+ context=self._context,
+ )
+
+ async def async_media_previous_track(self) -> None:
+ """Send previous track command."""
+ data = {ATTR_ENTITY_ID: self._features[KEY_TRACKS]}
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_MEDIA_PREVIOUS_TRACK,
+ data,
+ context=self._context,
+ )
+
+ async def async_media_seek(self, position: int) -> None:
+ """Send seek command."""
+ data = {
+ ATTR_ENTITY_ID: self._features[KEY_SEEK],
+ ATTR_MEDIA_SEEK_POSITION: position,
+ }
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_MEDIA_SEEK,
+ data,
+ context=self._context,
+ )
+
+ async def async_media_stop(self) -> None:
+ """Send stop command."""
+ data = {ATTR_ENTITY_ID: self._features[KEY_PAUSE_PLAY_STOP]}
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_MEDIA_STOP,
+ data,
+ context=self._context,
+ )
+
+ async def async_mute_volume(self, mute: bool) -> None:
+ """Mute the volume."""
+ data = {
+ ATTR_ENTITY_ID: self._features[KEY_VOLUME],
+ ATTR_MEDIA_VOLUME_MUTED: mute,
+ }
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_VOLUME_MUTE,
+ data,
+ context=self._context,
+ )
+
+ async def async_play_media(
+ self, media_type: str, media_id: str, **kwargs: Any
+ ) -> None:
+ """Play a piece of media."""
+ data = {
+ ATTR_ENTITY_ID: self._features[KEY_PLAY_MEDIA],
+ ATTR_MEDIA_CONTENT_ID: media_id,
+ ATTR_MEDIA_CONTENT_TYPE: media_type,
+ }
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_PLAY_MEDIA,
+ data,
+ context=self._context,
+ )
+
+ async def async_set_shuffle(self, shuffle: bool) -> None:
+ """Enable/disable shuffle mode."""
+ data = {
+ ATTR_ENTITY_ID: self._features[KEY_SHUFFLE],
+ ATTR_MEDIA_SHUFFLE: shuffle,
+ }
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_SHUFFLE_SET,
+ data,
+ context=self._context,
+ )
+
+ async def async_turn_on(self) -> None:
+ """Forward the turn_on command to all media in the media group."""
+ data = {ATTR_ENTITY_ID: self._features[KEY_ON_OFF]}
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_TURN_ON,
+ data,
+ context=self._context,
+ )
+
+ async def async_set_volume_level(self, volume: float) -> None:
+ """Set volume level(s)."""
+ data = {
+ ATTR_ENTITY_ID: self._features[KEY_VOLUME],
+ ATTR_MEDIA_VOLUME_LEVEL: volume,
+ }
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_VOLUME_SET,
+ data,
+ context=self._context,
+ )
+
+ async def async_turn_off(self) -> None:
+ """Forward the turn_off command to all media in the media group."""
+ data = {ATTR_ENTITY_ID: self._features[KEY_ON_OFF]}
+ await self.hass.services.async_call(
+ DOMAIN,
+ SERVICE_TURN_OFF,
+ data,
+ context=self._context,
+ )
+
+ async def async_volume_up(self) -> None:
+ """Turn volume up for media player(s)."""
+ for entity in self._features[KEY_VOLUME]:
+ volume_level = self.hass.states.get(entity).attributes["volume_level"] # type: ignore
+ if volume_level < 1:
+ await self.async_set_volume_level(min(1, volume_level + 0.1))
+
+ async def async_volume_down(self) -> None:
+ """Turn volume down for media player(s)."""
+ for entity in self._features[KEY_VOLUME]:
+ volume_level = self.hass.states.get(entity).attributes["volume_level"] # type: ignore
+ if volume_level > 0:
+ await self.async_set_volume_level(max(0, volume_level - 0.1))
+
+ @callback
+ def async_update_state(self) -> None:
+ """Query all members and determine the media group state."""
+ states = [self.hass.states.get(entity) for entity in self._entities]
+ states_values = [state.state for state in states if state is not None]
+ off_values = STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN
+
+ if states_values:
+ if states_values.count(states_values[0]) == len(states_values):
+ self._state = states_values[0]
+ elif any(state for state in states_values if state not in off_values):
+ self._state = STATE_ON
+ else:
+ self._state = STATE_OFF
+ else:
+ self._state = None
+
+ supported_features = 0
+ supported_features |= (
+ SUPPORT_CLEAR_PLAYLIST if self._features[KEY_CLEAR_PLAYLIST] else 0
+ )
+ supported_features |= (
+ SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK
+ if self._features[KEY_TRACKS]
+ else 0
+ )
+ supported_features |= (
+ SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP
+ if self._features[KEY_PAUSE_PLAY_STOP]
+ else 0
+ )
+ supported_features |= (
+ SUPPORT_PLAY_MEDIA if self._features[KEY_PLAY_MEDIA] else 0
+ )
+ supported_features |= SUPPORT_SEEK if self._features[KEY_SEEK] else 0
+ supported_features |= SUPPORT_SHUFFLE_SET if self._features[KEY_SHUFFLE] else 0
+ supported_features |= (
+ SUPPORT_TURN_ON | SUPPORT_TURN_OFF if self._features[KEY_ON_OFF] else 0
+ )
+ supported_features |= (
+ SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP
+ if self._features[KEY_VOLUME]
+ else 0
+ )
+
+ self._supported_features = supported_features
+ self.async_write_ha_state()
diff --git a/homeassistant/components/group/translations/he.json b/homeassistant/components/group/translations/he.json
index caa6ee98ea8..be7c2657e9f 100644
--- a/homeassistant/components/group/translations/he.json
+++ b/homeassistant/components/group/translations/he.json
@@ -13,5 +13,5 @@
"unlocked": "\u05e4\u05ea\u05d5\u05d7"
}
},
- "title": "\u05e7\u05b0\u05d1\u05d5\u05bc\u05e6\u05b8\u05d4"
+ "title": "\u05e7\u05d1\u05d5\u05e6\u05d4"
}
\ No newline at end of file
diff --git a/homeassistant/components/growatt_server/config_flow.py b/homeassistant/components/growatt_server/config_flow.py
index 300c96746e7..cc1457d3687 100644
--- a/homeassistant/components/growatt_server/config_flow.py
+++ b/homeassistant/components/growatt_server/config_flow.py
@@ -13,7 +13,6 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow class."""
VERSION = 1
- CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialise growatt server flow."""
diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py
index 881f0f46480..c8921d9e514 100644
--- a/homeassistant/components/growatt_server/sensor.py
+++ b/homeassistant/components/growatt_server/sensor.py
@@ -454,13 +454,13 @@ MIX_SENSOR_TYPES = {
),
"mix_wattage_pv_1": (
"PV1 Wattage",
- POWER_WATT,
+ POWER_KILO_WATT,
"pPv1",
{"device_class": DEVICE_CLASS_POWER},
),
"mix_wattage_pv_2": (
"PV2 Wattage",
- POWER_WATT,
+ POWER_KILO_WATT,
"pPv2",
{"device_class": DEVICE_CLASS_POWER},
),
diff --git a/homeassistant/components/growatt_server/translations/he.json b/homeassistant/components/growatt_server/translations/he.json
new file mode 100644
index 00000000000..cde5cec4fa4
--- /dev/null
+++ b/homeassistant/components/growatt_server/translations/he.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "\u05e9\u05dd",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/growatt_server/translations/hu.json b/homeassistant/components/growatt_server/translations/hu.json
new file mode 100644
index 00000000000..ff2c2fc87b5
--- /dev/null
+++ b/homeassistant/components/growatt_server/translations/hu.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json
index d987899463f..4de42e3190a 100644
--- a/homeassistant/components/gtfs/manifest.json
+++ b/homeassistant/components/gtfs/manifest.json
@@ -2,7 +2,7 @@
"domain": "gtfs",
"name": "General Transit Feed Specification (GTFS)",
"documentation": "https://www.home-assistant.io/integrations/gtfs",
- "requirements": ["pygtfs==0.1.5"],
+ "requirements": ["pygtfs==0.1.6"],
"codeowners": [],
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py
index d71a2fab67d..812e6a58f28 100644
--- a/homeassistant/components/gtfs/sensor.py
+++ b/homeassistant/components/gtfs/sensor.py
@@ -518,6 +518,8 @@ def setup_platform(
class GTFSDepartureSensor(SensorEntity):
"""Implementation of a GTFS departure sensor."""
+ _attr_device_class = DEVICE_CLASS_TIMESTAMP
+
def __init__(
self,
gtfs: Any,
@@ -576,11 +578,6 @@ class GTFSDepartureSensor(SensorEntity):
"""Icon to use in the frontend, if any."""
return self._icon
- @property
- def device_class(self) -> str:
- """Return the class of this device."""
- return DEVICE_CLASS_TIMESTAMP
-
def update(self) -> None:
"""Get the latest data from GTFS and update the states."""
with self.lock:
diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json
index 432afe8df27..fc3ca8fee06 100644
--- a/homeassistant/components/guardian/translations/de.json
+++ b/homeassistant/components/guardian/translations/de.json
@@ -6,6 +6,9 @@
"cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
+ "discovery_confirm": {
+ "description": "M\u00f6chten Sie dieses Guardian-Ger\u00e4t einrichten?"
+ },
"user": {
"data": {
"ip_address": "IP-Adresse",
diff --git a/homeassistant/components/guardian/translations/he.json b/homeassistant/components/guardian/translations/he.json
new file mode 100644
index 00000000000..8d77b3cd257
--- /dev/null
+++ b/homeassistant/components/guardian/translations/he.json
@@ -0,0 +1,17 @@
+{
+ "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",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py
index e8846d1f85a..efb82a9f1aa 100644
--- a/homeassistant/components/habitica/__init__.py
+++ b/homeassistant/components/habitica/__init__.py
@@ -19,6 +19,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
ATTR_ARGS,
+ ATTR_DATA,
ATTR_PATH,
CONF_API_USER,
DEFAULT_URL,
@@ -111,7 +112,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def handle_api_call(call):
name = call.data[ATTR_NAME]
path = call.data[ATTR_PATH]
- api = hass.data[DOMAIN].get(name)
+ entries = hass.config_entries.async_entries(DOMAIN)
+ api = None
+ for entry in entries:
+ if entry.data[CONF_NAME] == name:
+ api = hass.data[DOMAIN].get(entry.entry_id)
+ break
if api is None:
_LOGGER.error("API_CALL: User '%s' not configured", name)
return
@@ -126,7 +132,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
kwargs = call.data.get(ATTR_ARGS, {})
data = await api(**kwargs)
hass.bus.async_fire(
- EVENT_API_CALL_SUCCESS, {"name": name, "path": path, "data": data}
+ EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data}
)
data = hass.data.setdefault(DOMAIN, {})
diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py
index 02a46334c7a..1379f0a6447 100644
--- a/homeassistant/components/habitica/const.py
+++ b/homeassistant/components/habitica/const.py
@@ -7,7 +7,11 @@ CONF_API_USER = "api_user"
DEFAULT_URL = "https://habitica.com"
DOMAIN = "habitica"
+# service constants
SERVICE_API_CALL = "api_call"
ATTR_PATH = CONF_PATH
ATTR_ARGS = "args"
+
+# event constants
EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success"
+ATTR_DATA = "data"
diff --git a/homeassistant/components/habitica/translations/de.json b/homeassistant/components/habitica/translations/de.json
index 04f985946fb..ad4f3d2aff8 100644
--- a/homeassistant/components/habitica/translations/de.json
+++ b/homeassistant/components/habitica/translations/de.json
@@ -8,8 +8,11 @@
"user": {
"data": {
"api_key": "API-Schl\u00fcssel",
+ "api_user": "Habitica API-Benutzer-ID",
+ "name": "Override f\u00fcr den Benutzernamen von Habitica. Wird f\u00fcr Serviceaufrufe verwendet",
"url": "URL"
- }
+ },
+ "description": "Verbinden Sie Ihr Habitica-Profil, um die \u00dcberwachung des Profils und der Aufgaben Ihres Benutzers zu erm\u00f6glichen. Beachten Sie, dass api_id und api_key von https://habitica.com/user/settings/api bezogen werden m\u00fcssen."
}
}
},
diff --git a/homeassistant/components/habitica/translations/he.json b/homeassistant/components/habitica/translations/he.json
new file mode 100644
index 00000000000..9ef8ea8a345
--- /dev/null
+++ b/homeassistant/components/habitica/translations/he.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "\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": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "name": "\u05e2\u05e7\u05d5\u05e3 \u05e2\u05d1\u05d5\u05e8 \u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05dc Habitica. \u05d9\u05e9\u05de\u05e9 \u05e2\u05d1\u05d5\u05e8 \u05e7\u05e8\u05d9\u05d0\u05d5\u05ea \u05e9\u05d9\u05e8\u05d5\u05ea",
+ "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json
index 69cfa515c02..98531fca11a 100644
--- a/homeassistant/components/hangouts/manifest.json
+++ b/homeassistant/components/hangouts/manifest.json
@@ -3,7 +3,7 @@
"name": "Google Hangouts",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hangouts",
- "requirements": ["hangups==0.4.11"],
+ "requirements": ["hangups==0.4.14"],
"codeowners": [],
"iot_class": "cloud_push"
}
diff --git a/homeassistant/components/hangouts/translations/he.json b/homeassistant/components/hangouts/translations/he.json
index c3863a860f4..fa756c49ac6 100644
--- a/homeassistant/components/hangouts/translations/he.json
+++ b/homeassistant/components/hangouts/translations/he.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Google Hangouts \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
- "unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4."
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
},
"error": {
"invalid_2fa": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d1\u05d1\u05e7\u05e9\u05d4 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.",
@@ -14,13 +14,15 @@
"data": {
"2fa": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9"
},
+ "description": "\u05e8\u05d9\u05e7",
"title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9"
},
"user": {
"data": {
- "email": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d3\u05d5\u05d0\"\u05dc",
+ "email": "\u05d3\u05d5\u05d0\"\u05dc",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4"
},
+ "description": "\u05e8\u05d9\u05e7",
"title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc- Google Hangouts"
}
}
diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py
index d0172bf7378..e76e5559f9d 100644
--- a/homeassistant/components/harmony/__init__.py
+++ b/homeassistant/components/harmony/__init__.py
@@ -23,7 +23,7 @@ from .data import HarmonyData
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Logitech Harmony Hub from a config entry."""
# As there currently is no way to import options from yaml
# when setting up a config entry, we fallback to adding
diff --git a/homeassistant/components/harmony/translations/de.json b/homeassistant/components/harmony/translations/de.json
index 9cd07f09529..5083ccd848f 100644
--- a/homeassistant/components/harmony/translations/de.json
+++ b/homeassistant/components/harmony/translations/de.json
@@ -7,7 +7,7 @@
"cannot_connect": "Verbindung fehlgeschlagen",
"unknown": "Unerwarteter Fehler"
},
- "flow_title": "Logitech Harmony Hub {name}",
+ "flow_title": "{name}",
"step": {
"link": {
"description": "M\u00f6chten Sie {name} ({host}) einrichten?",
diff --git a/homeassistant/components/harmony/translations/he.json b/homeassistant/components/harmony/translations/he.json
new file mode 100644
index 00000000000..1331c17e961
--- /dev/null
+++ b/homeassistant/components/harmony/translations/he.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "link": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?"
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py
index d391817f964..6c71f2eb042 100644
--- a/homeassistant/components/hassio/__init__.py
+++ b/homeassistant/components/hassio/__init__.py
@@ -47,6 +47,7 @@ from .const import (
ATTR_URL,
ATTR_VERSION,
DOMAIN,
+ SupervisorEntityModel,
)
from .discovery import async_setup_discovery_view
from .handler import HassIO, HassioAPIError, api_data
@@ -222,6 +223,18 @@ async def async_start_addon(hass: HomeAssistant, slug: str) -> dict:
return await hassio.send_command(command, timeout=60)
+@bind_hass
+@api_data
+async def async_restart_addon(hass: HomeAssistant, slug: str) -> dict:
+ """Restart add-on.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio = hass.data[DOMAIN]
+ command = f"/addons/{slug}/restart"
+ return await hassio.send_command(command, timeout=None)
+
+
@bind_hass
@api_data
async def async_stop_addon(hass: HomeAssistant, slug: str) -> dict:
@@ -388,8 +401,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
if not user.is_admin:
await hass.auth.async_update_user(user, group_ids=[GROUP_ID_ADMIN])
+ # Migrate old name
+ if user.name == "Hass.io":
+ await hass.auth.async_update_user(user, name="Supervisor")
+
if refresh_token is None:
- user = await hass.auth.async_create_system_user("Hass.io", [GROUP_ID_ADMIN])
+ user = await hass.auth.async_create_system_user("Supervisor", [GROUP_ID_ADMIN])
refresh_token = await hass.auth.async_create_refresh_token(user)
data["hassio_user"] = user.id
await store.async_save(data)
@@ -581,7 +598,7 @@ def async_register_addons_in_dev_reg(
params = {
"config_entry_id": entry_id,
"identifiers": {(DOMAIN, addon[ATTR_SLUG])},
- "model": "Home Assistant Add-on",
+ "model": SupervisorEntityModel.ADDON,
"sw_version": addon[ATTR_VERSION],
"name": addon[ATTR_NAME],
"entry_type": ATTR_SERVICE,
@@ -600,7 +617,7 @@ def async_register_os_in_dev_reg(
"config_entry_id": entry_id,
"identifiers": {(DOMAIN, "OS")},
"manufacturer": "Home Assistant",
- "model": "Home Assistant Operating System",
+ "model": SupervisorEntityModel.OS,
"sw_version": os_dict[ATTR_VERSION],
"name": "Home Assistant Operating System",
"entry_type": ATTR_SERVICE,
@@ -609,9 +626,7 @@ def async_register_os_in_dev_reg(
@callback
-def async_remove_addons_from_dev_reg(
- dev_reg: DeviceRegistry, addons: list[dict[str, Any]]
-) -> None:
+def async_remove_addons_from_dev_reg(dev_reg: DeviceRegistry, addons: set[str]) -> None:
"""Remove addons from the device registry."""
for addon_slug in addons:
if dev := dev_reg.async_get_device({(DOMAIN, addon_slug)}):
@@ -668,16 +683,21 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
async_register_os_in_dev_reg(
self.entry_id, self.dev_reg, new_data["os"]
)
- return new_data
# Remove add-ons that are no longer installed from device registry
- if removed_addons := list(set(self.data["addons"]) - set(new_data["addons"])):
- async_remove_addons_from_dev_reg(self.dev_reg, removed_addons)
+ supervisor_addon_devices = {
+ list(device.identifiers)[0][1]
+ for device in self.dev_reg.devices.values()
+ if self.entry_id in device.config_entries
+ and device.model == SupervisorEntityModel.ADDON
+ }
+ if stale_addons := supervisor_addon_devices - set(new_data["addons"]):
+ async_remove_addons_from_dev_reg(self.dev_reg, stale_addons)
# If there are new add-ons, we should reload the config entry so we can
# create new devices and entities. We can return an empty dict because
# coordinator will be recreated.
- if list(set(new_data["addons"]) - set(self.data["addons"])):
+ if self.data and set(new_data["addons"]) - set(self.data["addons"]):
self.hass.async_create_task(
self.hass.config_entries.async_reload(self.entry_id)
)
diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py
index 435d42349fd..6104e57fb17 100644
--- a/homeassistant/components/hassio/const.py
+++ b/homeassistant/components/hassio/const.py
@@ -1,4 +1,5 @@
"""Hass.io const variables."""
+from enum import Enum
DOMAIN = "hassio"
@@ -46,3 +47,10 @@ ATTR_UPDATE_AVAILABLE = "update_available"
ATTR_SLUG = "slug"
ATTR_URL = "url"
ATTR_REPOSITORY = "repository"
+
+
+class SupervisorEntityModel(str, Enum):
+ """Supervisor entity model."""
+
+ ADDON = "Home Assistant Add-on"
+ OS = "Home Assistant Operating System"
diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py
index 61cec64bfda..b0c6e9d1dbe 100644
--- a/homeassistant/components/hassio/ingress.py
+++ b/homeassistant/components/hassio/ingress.py
@@ -7,7 +7,7 @@ import logging
import os
import aiohttp
-from aiohttp import hdrs, web
+from aiohttp import ClientTimeout, hdrs, web
from aiohttp.web_exceptions import HTTPBadGateway
from multidict import CIMultiDict
@@ -117,7 +117,6 @@ class HassIOIngress(HomeAssistantView):
) -> web.Response | web.StreamResponse:
"""Ingress route for request."""
url = self._create_url(token, path)
- data = await request.read()
source_header = _init_header(request, token)
async with self._websession.request(
@@ -126,7 +125,8 @@ class HassIOIngress(HomeAssistantView):
headers=source_header,
params=request.query,
allow_redirects=False,
- data=data,
+ data=request.content,
+ timeout=ClientTimeout(total=None),
) as result:
headers = _response_header(result)
@@ -168,6 +168,7 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st
if name in (
hdrs.CONTENT_LENGTH,
hdrs.CONTENT_ENCODING,
+ hdrs.TRANSFER_ENCODING,
hdrs.SEC_WEBSOCKET_EXTENSIONS,
hdrs.SEC_WEBSOCKET_PROTOCOL,
hdrs.SEC_WEBSOCKET_VERSION,
diff --git a/homeassistant/components/hassio/translations/he.json b/homeassistant/components/hassio/translations/he.json
new file mode 100644
index 00000000000..17b7fcd0050
--- /dev/null
+++ b/homeassistant/components/hassio/translations/he.json
@@ -0,0 +1,10 @@
+{
+ "system_health": {
+ "info": {
+ "host_os": "\u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4 \u05de\u05d0\u05e8\u05d7\u05ea",
+ "supervisor_api": "API \u05e9\u05dc \u05de\u05e4\u05e7\u05d7",
+ "supervisor_version": "\u05d2\u05d9\u05e8\u05e1\u05ea \u05de\u05e4\u05e7\u05d7",
+ "update_channel": "\u05e2\u05e8\u05d5\u05e5 \u05e2\u05d3\u05db\u05d5\u05df"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py
index ea0cac76a99..5de38675fca 100644
--- a/homeassistant/components/hdmi_cec/switch.py
+++ b/homeassistant/components/hdmi_cec/switch.py
@@ -2,7 +2,7 @@
import logging
from homeassistant.components.switch import DOMAIN, SwitchEntity
-from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY
+from homeassistant.const import STATE_OFF, STATE_ON
from . import ATTR_NEW, CecEntity
@@ -56,11 +56,6 @@ class CecSwitchEntity(CecEntity, SwitchEntity):
"""Return True if entity is on."""
return self._state == STATE_ON
- @property
- def is_standby(self):
- """Return true if device is in standby."""
- return self._state == STATE_OFF or self._state == STATE_STANDBY
-
@property
def state(self) -> str:
"""Return the cached state of device."""
diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py
index 3a9bedbb376..7490c1e5be1 100644
--- a/homeassistant/components/heos/__init__.py
+++ b/homeassistant/components/heos/__init__.py
@@ -67,7 +67,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Initialize config entry which represents the HEOS controller."""
# For backwards compat
if entry.unique_id is None:
diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py
index 63b592f1359..46a751983e9 100644
--- a/homeassistant/components/heos/media_player.py
+++ b/homeassistant/components/heos/media_player.py
@@ -1,7 +1,6 @@
"""Denon HEOS Media Player."""
from __future__ import annotations
-from collections.abc import Sequence
from functools import reduce, wraps
import logging
from operator import ior
@@ -362,7 +361,7 @@ class HeosMediaPlayer(MediaPlayerEntity):
return self._source_manager.get_current_source(self._player.now_playing_media)
@property
- def source_list(self) -> Sequence[str]:
+ def source_list(self) -> list[str]:
"""List of available input sources."""
return self._source_manager.source_list
diff --git a/homeassistant/components/heos/translations/he.json b/homeassistant/components/heos/translations/he.json
new file mode 100644
index 00000000000..2f2863169db
--- /dev/null
+++ b/homeassistant/components/heos/translations/he.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea."
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ },
+ "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4-IP \u05e9\u05dc \u05d4\u05ea\u05e7\u05df Heos (\u05e8\u05e6\u05d5\u05d9 \u05db\u05d6\u05d4 \u05d4\u05de\u05d7\u05d5\u05d1\u05e8 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d7\u05d5\u05d8 \u05dc\u05e8\u05e9\u05ea)."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hisense_aehw4a1/translations/he.json b/homeassistant/components/hisense_aehw4a1/translations/he.json
new file mode 100644
index 00000000000..380dbc5d7fc
--- /dev/null
+++ b/homeassistant/components/hisense_aehw4a1/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py
index ac8a13e69ad..8fa9fe879f5 100644
--- a/homeassistant/components/history/__init__.py
+++ b/homeassistant/components/history/__init__.py
@@ -14,7 +14,10 @@ import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.recorder import history, models as history_models
-from homeassistant.components.recorder.statistics import statistics_during_period
+from homeassistant.components.recorder.statistics import (
+ list_statistic_ids,
+ statistics_during_period,
+)
from homeassistant.components.recorder.util import session_scope
from homeassistant.const import (
CONF_DOMAINS,
@@ -105,6 +108,7 @@ async def async_setup(hass, config):
hass.components.websocket_api.async_register_command(
ws_get_statistics_during_period
)
+ hass.components.websocket_api.async_register_command(ws_get_list_statistic_ids)
return True
@@ -119,7 +123,7 @@ class LazyState(history_models.LazyState):
vol.Required("type"): "history/statistics_during_period",
vol.Required("start_time"): str,
vol.Optional("end_time"): str,
- vol.Optional("statistic_id"): str,
+ vol.Optional("statistic_ids"): [str],
}
)
@websocket_api.async_response
@@ -152,9 +156,29 @@ async def ws_get_statistics_during_period(
hass,
start_time,
end_time,
- msg.get("statistic_id"),
+ msg.get("statistic_ids"),
)
- connection.send_result(msg["id"], {"statistics": statistics})
+ connection.send_result(msg["id"], statistics)
+
+
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "history/list_statistic_ids",
+ vol.Optional("statistic_type"): str,
+ }
+)
+@websocket_api.require_admin
+@websocket_api.async_response
+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(
+ list_statistic_ids,
+ hass,
+ msg.get("statistic_type"),
+ )
+ connection.send_result(msg["id"], statistic_ids)
class HistoryPeriodView(HomeAssistantView):
diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py
index 518f3286231..f21afc51801 100644
--- a/homeassistant/components/hive/sensor.py
+++ b/homeassistant/components/hive/sensor.py
@@ -1,4 +1,4 @@
-"""Support for the Hive sesnors."""
+"""Support for the Hive sensors."""
from datetime import timedelta
diff --git a/homeassistant/components/hive/translations/he.json b/homeassistant/components/hive/translations/he.json
new file mode 100644
index 00000000000..dcc967c6986
--- /dev/null
+++ b/homeassistant/components/hive/translations/he.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "invalid_password": "\u05d4\u05db\u05e0\u05d9\u05e1\u05d4 \u05dc-Hive \u05e0\u05db\u05e9\u05dc\u05d4. \u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4. \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05e0\u05d9\u05ea.",
+ "invalid_username": "\u05d4\u05db\u05e0\u05d9\u05e1\u05d4 \u05dc\u05db\u05d5\u05d5\u05e8\u05ea \u05e0\u05db\u05e9\u05dc\u05d4. \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d3\u05d5\u05d0\u05e8 \u05d4\u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9 \u05e9\u05dc\u05da \u05d0\u05d9\u05e0\u05d4 \u05de\u05d6\u05d5\u05d4\u05d4.",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hlk_sw16/translations/he.json b/homeassistant/components/hlk_sw16/translations/he.json
new file mode 100644
index 00000000000..479d2f2f5e8
--- /dev/null
+++ b/homeassistant/components/hlk_sw16/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "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",
+ "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": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_connect/translations/he.json b/homeassistant/components/home_connect/translations/he.json
new file mode 100644
index 00000000000..6051eb96eb2
--- /dev/null
+++ b/homeassistant/components/home_connect/translations/he.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+ "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})"
+ },
+ "create_entry": {
+ "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py
index 176dc2fbd02..954203e9b10 100644
--- a/homeassistant/components/home_plus_control/__init__.py
+++ b/homeassistant/components/home_plus_control/__init__.py
@@ -66,22 +66,20 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
return True
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Legrand Home+ Control from a config entry."""
- hass_entry_data = hass.data[DOMAIN].setdefault(config_entry.entry_id, {})
+ hass_entry_data = hass.data[DOMAIN].setdefault(entry.entry_id, {})
# Retrieve the registered implementation
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
- hass, config_entry
+ hass, entry
)
)
# Using an aiohttp-based API lib, so rely on async framework
# Add the API object to the domain's data in HA
- api = hass_entry_data[API] = HomePlusControlAsyncApi(
- hass, config_entry, implementation
- )
+ api = hass_entry_data[API] = HomePlusControlAsyncApi(hass, entry, implementation)
# Set of entity unique identifiers of this integration
uids = hass_entry_data[ENTITY_UIDS] = set()
@@ -143,7 +141,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
"""Continue setting up the platforms."""
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
for platform in PLATFORMS
]
)
diff --git a/homeassistant/components/home_plus_control/helpers.py b/homeassistant/components/home_plus_control/helpers.py
index 773732b1a50..f5687a23c66 100644
--- a/homeassistant/components/home_plus_control/helpers.py
+++ b/homeassistant/components/home_plus_control/helpers.py
@@ -20,7 +20,7 @@ class HomePlusControlOAuth2Implementation(
subscription_key (str): Subscription key obtained from the API provider.
authorize_url (str): Authorization URL initiate authentication flow.
token_url (str): URL to retrieve access/refresh tokens.
- name (str): Name of the implementation (appears in the HomeAssitant GUI).
+ name (str): Name of the implementation (appears in the HomeAssistant GUI).
"""
def __init__(
diff --git a/homeassistant/components/home_plus_control/translations/he.json b/homeassistant/components/home_plus_control/translations/he.json
new file mode 100644
index 00000000000..2800ddd7e62
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/he.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea",
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+ "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})",
+ "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."
+ },
+ "create_entry": {
+ "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/he.json b/homeassistant/components/homeassistant/translations/he.json
index f45b17b1a13..f86d7b0dca0 100644
--- a/homeassistant/components/homeassistant/translations/he.json
+++ b/homeassistant/components/homeassistant/translations/he.json
@@ -1,7 +1,14 @@
{
"system_health": {
"info": {
- "os_name": "\u05de\u05e9\u05e4\u05d7\u05ea \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4"
+ "docker": "Docker",
+ "hassio": "\u05de\u05e4\u05e7\u05d7",
+ "installation_type": "\u05e1\u05d5\u05d2 \u05d4\u05ea\u05e7\u05e0\u05d4",
+ "os_name": "\u05de\u05e9\u05e4\u05d7\u05ea \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4",
+ "os_version": "\u05d2\u05d9\u05e8\u05e1\u05ea \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4",
+ "python_version": "\u05d2\u05e8\u05e1\u05ea \u05e4\u05d9\u05d9\u05ea\u05d5\u05df",
+ "timezone": "\u05d0\u05d6\u05d5\u05e8 \u05d6\u05de\u05df",
+ "version": "\u05d2\u05d9\u05e8\u05e1\u05d4"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py
index 2e78a93315d..47dc5317bbd 100644
--- a/homeassistant/components/homeassistant/triggers/event.py
+++ b/homeassistant/components/homeassistant/triggers/event.py
@@ -1,16 +1,20 @@
"""Offer event listening automation rules."""
+from __future__ import annotations
+
+from typing import Any
+
import voluptuous as vol
+from homeassistant.components.automation import AutomationActionType
from homeassistant.const import CONF_EVENT_DATA, CONF_PLATFORM
-from homeassistant.core import HassJob, callback
+from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, template
-
-# mypy: allow-untyped-defs
+from homeassistant.helpers.typing import ConfigType
CONF_EVENT_TYPE = "event_type"
CONF_EVENT_CONTEXT = "context"
-TRIGGER_SCHEMA = vol.Schema(
+TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "event",
vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.template]),
@@ -20,7 +24,7 @@ TRIGGER_SCHEMA = vol.Schema(
)
-def _schema_value(value):
+def _schema_value(value: Any) -> Any:
if isinstance(value, list):
return vol.In(value)
@@ -28,10 +32,15 @@ def _schema_value(value):
async def async_attach_trigger(
- hass, config, action, automation_info, *, platform_type="event"
-):
+ hass: HomeAssistant,
+ config: ConfigType,
+ action: AutomationActionType,
+ automation_info: dict[str, Any],
+ *,
+ platform_type: str = "event",
+) -> CALLBACK_TYPE:
"""Listen for events based on configuration."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
variables = None
if automation_info:
variables = automation_info.get("variables")
@@ -76,7 +85,7 @@ async def async_attach_trigger(
job = HassJob(action)
@callback
- def handle_event(event):
+ def handle_event(event: Event) -> None:
"""Listen for events and calls the action when data matches."""
try:
# Check that the event data and context match the configured
@@ -93,10 +102,10 @@ async def async_attach_trigger(
job,
{
"trigger": {
+ **trigger_data,
"platform": platform_type,
"event": event,
"description": f"event '{event.event_type}'",
- "id": trigger_id,
}
},
event.context,
@@ -107,7 +116,7 @@ async def async_attach_trigger(
]
@callback
- def remove_listen_events():
+ def remove_listen_events() -> None:
"""Remove event listeners."""
for remove in removes:
remove()
diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py
index 2f3ae8e6ad2..ea1a985139f 100644
--- a/homeassistant/components/homeassistant/triggers/homeassistant.py
+++ b/homeassistant/components/homeassistant/triggers/homeassistant.py
@@ -3,13 +3,14 @@ import voluptuous as vol
from homeassistant.const import CONF_EVENT, CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HassJob, callback
+from homeassistant.helpers import config_validation as cv
# mypy: allow-untyped-defs
EVENT_START = "start"
EVENT_SHUTDOWN = "shutdown"
-TRIGGER_SCHEMA = vol.Schema(
+TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "homeassistant",
vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN),
@@ -19,7 +20,7 @@ TRIGGER_SCHEMA = vol.Schema(
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for events based on configuration."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
event = config.get(CONF_EVENT)
job = HassJob(action)
@@ -32,10 +33,10 @@ async def async_attach_trigger(hass, config, action, automation_info):
job,
{
"trigger": {
+ **trigger_data,
"platform": "homeassistant",
"event": event,
"description": "Home Assistant stopping",
- "id": trigger_id,
}
},
event.context,
@@ -50,10 +51,10 @@ async def async_attach_trigger(hass, config, action, automation_info):
job,
{
"trigger": {
+ **trigger_data,
"platform": "homeassistant",
"event": event,
"description": "Home Assistant starting",
- "id": trigger_id,
}
},
)
diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py
index 05eed9ee27b..366f937a192 100644
--- a/homeassistant/components/homeassistant/triggers/numeric_state.py
+++ b/homeassistant/components/homeassistant/triggers/numeric_state.py
@@ -44,7 +44,7 @@ def validate_above_below(value):
TRIGGER_SCHEMA = vol.All(
- vol.Schema(
+ cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "numeric_state",
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
@@ -78,7 +78,7 @@ async def async_attach_trigger(
attribute = config.get(CONF_ATTRIBUTE)
job = HassJob(action)
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
_variables = {}
if automation_info:
_variables = automation_info.get("variables") or {}
@@ -132,6 +132,7 @@ async def async_attach_trigger(
job,
{
"trigger": {
+ **trigger_data,
"platform": platform_type,
"entity_id": entity_id,
"below": below,
@@ -140,7 +141,6 @@ async def async_attach_trigger(
"to_state": to_s,
"for": time_delta if not time_delta else period[entity_id],
"description": f"numeric state of {entity_id}",
- "id": trigger_id,
}
},
to_s.context,
diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py
index 69cddbfe126..2c96b6be944 100644
--- a/homeassistant/components/homeassistant/triggers/state.py
+++ b/homeassistant/components/homeassistant/triggers/state.py
@@ -27,25 +27,25 @@ CONF_ENTITY_ID = "entity_id"
CONF_FROM = "from"
CONF_TO = "to"
-BASE_SCHEMA = {
- vol.Required(CONF_PLATFORM): "state",
- vol.Required(CONF_ENTITY_ID): cv.entity_ids,
- vol.Optional(CONF_FOR): cv.positive_time_period_template,
- vol.Optional(CONF_ATTRIBUTE): cv.match_all,
-}
-
-TRIGGER_STATE_SCHEMA = vol.Schema(
+BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_PLATFORM): "state",
+ vol.Required(CONF_ENTITY_ID): cv.entity_ids,
+ vol.Optional(CONF_FOR): cv.positive_time_period_template,
+ vol.Optional(CONF_ATTRIBUTE): cv.match_all,
+ }
+)
+
+TRIGGER_STATE_SCHEMA = BASE_SCHEMA.extend(
{
- **BASE_SCHEMA,
# These are str on purpose. Want to catch YAML conversions
vol.Optional(CONF_FROM): vol.Any(str, [str]),
vol.Optional(CONF_TO): vol.Any(str, [str]),
}
)
-TRIGGER_ATTRIBUTE_SCHEMA = vol.Schema(
+TRIGGER_ATTRIBUTE_SCHEMA = BASE_SCHEMA.extend(
{
- **BASE_SCHEMA,
vol.Optional(CONF_FROM): cv.match_all,
vol.Optional(CONF_TO): cv.match_all,
}
@@ -87,7 +87,7 @@ async def async_attach_trigger(
attribute = config.get(CONF_ATTRIBUTE)
job = HassJob(action)
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
_variables = {}
if automation_info:
_variables = automation_info.get("variables") or {}
@@ -134,6 +134,7 @@ async def async_attach_trigger(
job,
{
"trigger": {
+ **trigger_data,
"platform": platform_type,
"entity_id": entity,
"from_state": from_s,
@@ -141,7 +142,6 @@ async def async_attach_trigger(
"for": time_delta if not time_delta else period[entity],
"attribute": attribute,
"description": f"state of {entity}",
- "id": trigger_id,
}
},
event.context,
diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py
index 6668672732e..ff78e4c43c8 100644
--- a/homeassistant/components/homeassistant/triggers/time.py
+++ b/homeassistant/components/homeassistant/triggers/time.py
@@ -29,7 +29,7 @@ _TIME_TRIGGER_SCHEMA = vol.Any(
msg="Expected HH:MM, HH:MM:SS or Entity ID with domain 'input_datetime' or 'sensor'",
)
-TRIGGER_SCHEMA = vol.Schema(
+TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "time",
vol.Required(CONF_AT): vol.All(cv.ensure_list, [_TIME_TRIGGER_SCHEMA]),
@@ -39,7 +39,7 @@ TRIGGER_SCHEMA = vol.Schema(
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
entities = {}
removes = []
job = HassJob(action)
@@ -51,11 +51,11 @@ async def async_attach_trigger(hass, config, action, automation_info):
job,
{
"trigger": {
+ **trigger_data,
"platform": "time",
"now": now,
"description": description,
"entity_id": entity_id,
- "id": trigger_id,
}
},
)
diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py
index 859f76b773b..0380e01c239 100644
--- a/homeassistant/components/homeassistant/triggers/time_pattern.py
+++ b/homeassistant/components/homeassistant/triggers/time_pattern.py
@@ -43,7 +43,7 @@ class TimePattern:
TRIGGER_SCHEMA = vol.All(
- vol.Schema(
+ cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "time_pattern",
CONF_HOURS: TimePattern(maximum=23),
@@ -57,7 +57,7 @@ TRIGGER_SCHEMA = vol.All(
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
hours = config.get(CONF_HOURS)
minutes = config.get(CONF_MINUTES)
seconds = config.get(CONF_SECONDS)
@@ -76,10 +76,10 @@ async def async_attach_trigger(hass, config, action, automation_info):
job,
{
"trigger": {
+ **trigger_data,
"platform": "time_pattern",
"now": now,
"description": "time pattern",
- "id": trigger_id,
}
},
)
diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py
index ef228f3ae60..c0cc5867799 100644
--- a/homeassistant/components/homekit/__init__.py
+++ b/homeassistant/components/homekit/__init__.py
@@ -59,7 +59,7 @@ from . import ( # noqa: F401
from .accessories import HomeBridge, HomeDriver, get_accessory
from .aidmanager import AccessoryAidStorage
from .const import (
- ATTR_INTERGRATION,
+ ATTR_INTEGRATION,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_SOFTWARE_VERSION,
@@ -231,7 +231,7 @@ def _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf):
return False
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HomeKit from a config entry."""
_async_import_options_from_data_if_missing(hass, entry)
@@ -297,7 +297,7 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
await hass.config_entries.async_reload(entry.entry_id)
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
dismiss_setup_message(hass, entry.entry_id)
homekit = hass.data[DOMAIN][entry.entry_id][HOMEKIT]
@@ -599,7 +599,8 @@ class HomeKit:
await self.hass.async_add_executor_job(self.setup, async_zc_instance)
self.aid_storage = AccessoryAidStorage(self.hass, self._entry_id)
await self.aid_storage.async_initialize()
- await self._async_create_accessories()
+ if not await self._async_create_accessories():
+ return
self._async_register_bridge()
_LOGGER.debug("Driver start for %s", self._name)
await self.driver.async_start()
@@ -666,6 +667,13 @@ class HomeKit:
"""Create the accessories."""
entity_states = await self.async_configure_accessories()
if self._homekit_mode == HOMEKIT_MODE_ACCESSORY:
+ if not entity_states:
+ _LOGGER.error(
+ "HomeKit %s cannot startup: entity not available: %s",
+ self._name,
+ self._filter.config,
+ )
+ return False
state = entity_states[0]
conf = self._config.pop(state.entity_id, {})
acc = get_accessory(self.hass, self.driver, state, STANDALONE_AID, conf)
@@ -677,6 +685,7 @@ class HomeKit:
# No need to load/persist as we do it in setup
self.driver.accessory = acc
+ return True
async def async_stop(self, *args):
"""Stop the accessory driver."""
@@ -767,9 +776,9 @@ class HomeKit:
integration = await async_get_integration(
self.hass, ent_reg_ent.platform
)
- ent_cfg[ATTR_INTERGRATION] = integration.name
+ ent_cfg[ATTR_INTEGRATION] = integration.name
except IntegrationNotFound:
- ent_cfg[ATTR_INTERGRATION] = ent_reg_ent.platform
+ ent_cfg[ATTR_INTEGRATION] = ent_reg_ent.platform
class HomeKitPairingQRView(HomeAssistantView):
diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py
index 3aeaa31faed..b9bd62246cf 100644
--- a/homeassistant/components/homekit/accessories.py
+++ b/homeassistant/components/homekit/accessories.py
@@ -42,7 +42,7 @@ from homeassistant.util.decorator import Registry
from .const import (
ATTR_DISPLAY_NAME,
- ATTR_INTERGRATION,
+ ATTR_INTEGRATION,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_SOFTWARE_VERSION,
@@ -221,8 +221,8 @@ class HomeAccessory(Accessory):
if ATTR_MANUFACTURER in self.config:
manufacturer = self.config[ATTR_MANUFACTURER]
- elif ATTR_INTERGRATION in self.config:
- manufacturer = self.config[ATTR_INTERGRATION].replace("_", " ").title()
+ elif ATTR_INTEGRATION in self.config:
+ manufacturer = self.config[ATTR_INTEGRATION].replace("_", " ").title()
else:
manufacturer = f"{MANUFACTURER} {domain}".title()
if ATTR_MODEL in self.config:
diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py
index 073650aba40..37788f9dca7 100644
--- a/homeassistant/components/homekit/const.py
+++ b/homeassistant/components/homekit/const.py
@@ -21,7 +21,7 @@ AUDIO_CODEC_COPY = "copy"
# #### Attributes ####
ATTR_DISPLAY_NAME = "display_name"
ATTR_VALUE = "value"
-ATTR_INTERGRATION = "platform"
+ATTR_INTEGRATION = "platform"
ATTR_MANUFACTURER = "manufacturer"
ATTR_MODEL = "model"
ATTR_SOFTWARE_VERSION = "sw_version"
diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json
index d2c2f094a0f..39c40e03614 100644
--- a/homeassistant/components/homekit/manifest.json
+++ b/homeassistant/components/homekit/manifest.json
@@ -3,7 +3,7 @@
"name": "HomeKit",
"documentation": "https://www.home-assistant.io/integrations/homekit",
"requirements": [
- "HAP-python==3.5.0",
+ "HAP-python==3.5.1",
"fnvhash==0.1.0",
"PyQRCode==1.2.1",
"base36==0.1.1",
diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json
index 09a6b059ea7..e115c932ac4 100644
--- a/homeassistant/components/homekit/translations/de.json
+++ b/homeassistant/components/homekit/translations/de.json
@@ -13,7 +13,7 @@
"include_domains": "Einzubeziehende Domains"
},
"description": "W\u00e4hlen Sie die Domains aus, die aufgenommen werden sollen. Alle unterst\u00fctzten Entit\u00e4ten in der Domain werden aufgenommen. F\u00fcr jeden TV-Mediaplayer und jede Kamera wird eine separate HomeKit-Instanz im Zubeh\u00f6rmodus erstellt.",
- "title": "HomeKit aktivieren"
+ "title": "W\u00e4hle die zu einzubeziehenden Dom\u00e4nen aus."
}
}
},
@@ -47,7 +47,7 @@
"mode": "Modus"
},
"description": "HomeKit kann so konfiguriert werden, dass eine Br\u00fccke oder ein einzelnes Zubeh\u00f6r verf\u00fcgbar gemacht wird. Im Zubeh\u00f6rmodus kann nur eine einzelne Entit\u00e4t verwendet werden. F\u00fcr Media Player mit der TV-Ger\u00e4teklasse ist ein Zubeh\u00f6rmodus erforderlich, damit sie ordnungsgem\u00e4\u00df funktionieren. Entit\u00e4ten in den \"einzuschlie\u00dfenden Dom\u00e4nen\" werden f\u00fcr HomeKit verf\u00fcgbar gemacht. Auf dem n\u00e4chsten Bildschirm k\u00f6nnen Sie ausw\u00e4hlen, welche Entit\u00e4ten in diese Liste aufgenommen oder aus dieser ausgeschlossen werden sollen.",
- "title": "W\u00e4hle die zu \u00fcberbr\u00fcckenden Dom\u00e4nen aus."
+ "title": "W\u00e4hle die zu einzubeziehenden Dom\u00e4nen aus."
},
"yaml": {
"description": "Dieser Eintrag wird \u00fcber YAML gesteuert",
diff --git a/homeassistant/components/homekit/translations/he.json b/homeassistant/components/homekit/translations/he.json
index cb5a530b739..789298b7705 100644
--- a/homeassistant/components/homekit/translations/he.json
+++ b/homeassistant/components/homekit/translations/he.json
@@ -1,4 +1,13 @@
{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "include_domains": "\u05ea\u05d7\u05d5\u05de\u05d9\u05dd \u05e9\u05d9\u05e9 \u05dc\u05db\u05dc\u05d5\u05dc"
+ }
+ }
+ }
+ },
"options": {
"step": {
"include_exclude": {
@@ -9,6 +18,7 @@
},
"init": {
"data": {
+ "include_domains": "\u05ea\u05d7\u05d5\u05de\u05d9\u05dd \u05e9\u05d9\u05e9 \u05dc\u05db\u05dc\u05d5\u05dc",
"mode": "\u05de\u05e6\u05d1"
}
},
diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py
index 3db6c1800c9..f7507d09837 100644
--- a/homeassistant/components/homekit_controller/__init__.py
+++ b/homeassistant/components/homekit_controller/__init__.py
@@ -32,6 +32,8 @@ def escape_characteristic_name(char_name):
class HomeKitEntity(Entity):
"""Representation of a Home Assistant HomeKit device."""
+ _attr_should_poll = False
+
def __init__(self, accessory, devinfo):
"""Initialise a generic HomeKit device."""
self._accessory = accessory
@@ -99,14 +101,6 @@ class HomeKitEntity(Entity):
payload = self.service.build_update(characteristics)
return await self._accessory.put_characteristics(payload)
- @property
- def should_poll(self) -> bool:
- """Return False.
-
- Data update is triggered from HKDevice.
- """
- return False
-
def setup(self):
"""Configure an entity baed on its HomeKit characteristics metadata."""
self.pollable_characteristics = []
@@ -225,8 +219,10 @@ async def async_setup(hass, config):
map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass)
await map_storage.async_initialize()
- zeroconf_instance = await zeroconf.async_get_instance(hass)
- hass.data[CONTROLLER] = aiohomekit.Controller(zeroconf_instance=zeroconf_instance)
+ async_zeroconf_instance = await zeroconf.async_get_async_instance(hass)
+ hass.data[CONTROLLER] = aiohomekit.Controller(
+ async_zeroconf_instance=async_zeroconf_instance
+ )
hass.data[KNOWN_DEVICES] = {}
hass.data[TRIGGERS] = {}
diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py
index 537e9c2a698..64257c47f47 100644
--- a/homeassistant/components/homekit_controller/binary_sensor.py
+++ b/homeassistant/components/homekit_controller/binary_sensor.py
@@ -19,15 +19,12 @@ from . import KNOWN_DEVICES, HomeKitEntity
class HomeKitMotionSensor(HomeKitEntity, BinarySensorEntity):
"""Representation of a Homekit motion sensor."""
+ _attr_device_class = DEVICE_CLASS_MOTION
+
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.MOTION_DETECTED]
- @property
- def device_class(self):
- """Define this binary_sensor as a motion sensor."""
- return DEVICE_CLASS_MOTION
-
@property
def is_on(self):
"""Has motion been detected."""
@@ -37,15 +34,12 @@ class HomeKitMotionSensor(HomeKitEntity, BinarySensorEntity):
class HomeKitContactSensor(HomeKitEntity, BinarySensorEntity):
"""Representation of a Homekit contact sensor."""
+ _attr_device_class = DEVICE_CLASS_OPENING
+
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.CONTACT_STATE]
- @property
- def device_class(self):
- """Define this binary_sensor as a opening sensor."""
- return DEVICE_CLASS_OPENING
-
@property
def is_on(self):
"""Return true if the binary sensor is on/open."""
@@ -55,10 +49,7 @@ class HomeKitContactSensor(HomeKitEntity, BinarySensorEntity):
class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity):
"""Representation of a Homekit smoke sensor."""
- @property
- def device_class(self) -> str:
- """Return the class of this sensor."""
- return DEVICE_CLASS_SMOKE
+ _attr_device_class = DEVICE_CLASS_SMOKE
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
@@ -73,10 +64,7 @@ class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity):
class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity):
"""Representation of a Homekit BO sensor."""
- @property
- def device_class(self) -> str:
- """Return the class of this sensor."""
- return DEVICE_CLASS_GAS
+ _attr_device_class = DEVICE_CLASS_GAS
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
@@ -91,10 +79,7 @@ class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity):
class HomeKitOccupancySensor(HomeKitEntity, BinarySensorEntity):
"""Representation of a Homekit occupancy sensor."""
- @property
- def device_class(self) -> str:
- """Return the class of this sensor."""
- return DEVICE_CLASS_OCCUPANCY
+ _attr_device_class = DEVICE_CLASS_OCCUPANCY
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
@@ -109,15 +94,12 @@ class HomeKitOccupancySensor(HomeKitEntity, BinarySensorEntity):
class HomeKitLeakSensor(HomeKitEntity, BinarySensorEntity):
"""Representation of a Homekit leak sensor."""
+ _attr_device_class = DEVICE_CLASS_MOISTURE
+
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.LEAK_DETECTED]
- @property
- def device_class(self):
- """Define this binary_sensor as a leak sensor."""
- return DEVICE_CLASS_MOISTURE
-
@property
def is_on(self):
"""Return true if a leak is detected from the binary sensor."""
diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py
index a2c9c2540ac..ec0f383356e 100644
--- a/homeassistant/components/homekit_controller/climate.py
+++ b/homeassistant/components/homekit_controller/climate.py
@@ -19,6 +19,7 @@ from homeassistant.components.climate import (
ClimateEntity,
)
from homeassistant.components.climate.const import (
+ ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
CURRENT_HVAC_COOL,
@@ -36,7 +37,7 @@ from homeassistant.components.climate.const import (
SWING_OFF,
SWING_VERTICAL,
)
-from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS
+from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.core import callback
from . import KNOWN_DEVICES, HomeKitEntity
@@ -323,11 +324,6 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
"""Return the unit of measurement."""
return TEMP_CELSIUS
- @property
- def precision(self):
- """Return the precision of the system."""
- return PRECISION_TENTHS
-
class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
"""Representation of a Homekit climate device."""
@@ -347,16 +343,27 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
+ chars = {}
+
+ value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
+ mode = MODE_HOMEKIT_TO_HASS.get(value)
+
+ if kwargs.get(ATTR_HVAC_MODE, mode) != mode:
+ mode = kwargs[ATTR_HVAC_MODE]
+ chars[CharacteristicsTypes.HEATING_COOLING_TARGET] = MODE_HASS_TO_HOMEKIT[
+ mode
+ ]
+
temp = kwargs.get(ATTR_TEMPERATURE)
heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
- value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
- if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and (
+
+ if (mode == HVAC_MODE_HEAT_COOL) and (
SUPPORT_TARGET_TEMPERATURE_RANGE & self.supported_features
):
if temp is None:
temp = (cool_temp + heat_temp) / 2
- await self.async_put_characteristics(
+ chars.update(
{
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD: heat_temp,
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: cool_temp,
@@ -364,9 +371,9 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
}
)
else:
- await self.async_put_characteristics(
- {CharacteristicsTypes.TEMPERATURE_TARGET: temp}
- )
+ chars[CharacteristicsTypes.TEMPERATURE_TARGET] = temp
+
+ await self.async_put_characteristics(chars)
async def async_set_humidity(self, humidity):
"""Set new target humidity."""
@@ -541,11 +548,6 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
"""Return the unit of measurement."""
return TEMP_CELSIUS
- @property
- def precision(self):
- """Return the precision of the system."""
- return PRECISION_TENTHS
-
ENTITY_TYPES = {
ServicesTypes.HEATER_COOLER: HomeKitHeaterCoolerEntity,
diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py
index 6ae66d362c9..e8357a4001d 100644
--- a/homeassistant/components/homekit_controller/config_flow.py
+++ b/homeassistant/components/homekit_controller/config_flow.py
@@ -99,8 +99,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def _async_setup_controller(self):
"""Create the controller."""
- zeroconf_instance = await zeroconf.async_get_instance(self.hass)
- self.controller = aiohomekit.Controller(zeroconf_instance=zeroconf_instance)
+ async_zeroconf_instance = await zeroconf.async_get_async_instance(self.hass)
+ self.controller = aiohomekit.Controller(
+ async_zeroconf_instance=async_zeroconf_instance
+ )
async def async_step_user(self, user_input=None):
"""Handle a flow start."""
@@ -234,9 +236,20 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
)
config_num = None
+ # Set unique-id and error out if it's already configured
+ existing_entry = await self.async_set_unique_id(normalize_hkid(hkid))
+ updated_ip_port = {
+ "AccessoryIP": discovery_info["host"],
+ "AccessoryPort": discovery_info["port"],
+ }
+
# If the device is already paired and known to us we should monitor c#
# (config_num) for changes. If it changes, we check for new entities
if paired and hkid in self.hass.data.get(KNOWN_DEVICES, {}):
+ if existing_entry:
+ self.hass.config_entries.async_update_entry(
+ existing_entry, data={**existing_entry.data, **updated_ip_port}
+ )
conn = self.hass.data[KNOWN_DEVICES][hkid]
# When we rediscover the device, let aiohomekit know
# that the device is available and we should not wait
@@ -260,8 +273,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await self.hass.config_entries.async_remove(existing.entry_id)
# Set unique-id and error out if it's already configured
- await self.async_set_unique_id(normalize_hkid(hkid))
- self._abort_if_unique_id_configured()
+ self._abort_if_unique_id_configured(updates=updated_ip_port)
self.context["hkid"] = hkid
diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py
index a3f7a9b7921..9fbc8fc4c62 100644
--- a/homeassistant/components/homekit_controller/const.py
+++ b/homeassistant/components/homekit_controller/const.py
@@ -44,5 +44,7 @@ HOMEKIT_ACCESSORY_DISPATCH = {
}
CHARACTERISTIC_PLATFORMS = {
+ CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: "sensor",
CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: "sensor",
+ CharacteristicsTypes.get_uuid(CharacteristicsTypes.TEMPERATURE_CURRENT): "sensor",
}
diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py
index a04d7237cf1..818b75e47d3 100644
--- a/homeassistant/components/homekit_controller/device_trigger.py
+++ b/homeassistant/components/homekit_controller/device_trigger.py
@@ -8,7 +8,7 @@ from aiohomekit.utils import clamp_enum_to_char
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.typing import ConfigType
@@ -33,7 +33,7 @@ TRIGGER_SUBTYPES = {"single_press", "double_press", "long_press"}
CONF_IID = "iid"
CONF_SUBTYPE = "subtype"
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
vol.Required(CONF_SUBTYPE): vol.In(TRIGGER_SUBTYPES),
@@ -76,13 +76,15 @@ class TriggerSource:
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = (
+ automation_info.get("trigger_data", {}) if automation_info else {}
+ )
def event_handler(char):
if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]:
return
self._hass.async_create_task(
- action({"trigger": {**config, "id": trigger_id}})
+ action({"trigger": {**trigger_data, **config}})
)
trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]]
diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py
index 227174d00e9..dfddd29f2ff 100644
--- a/homeassistant/components/homekit_controller/humidifier.py
+++ b/homeassistant/components/homekit_controller/humidifier.py
@@ -35,6 +35,8 @@ HA_MODE_TO_HK = {
class HomeKitHumidifier(HomeKitEntity, HumidifierEntity):
"""Representation of a HomeKit Controller Humidifier."""
+ _attr_device_class = DEVICE_CLASS_HUMIDIFIER
+
def get_characteristic_types(self):
"""Define the homekit characteristics the entity cares about."""
return [
@@ -45,11 +47,6 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity):
CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD,
]
- @property
- def device_class(self) -> str:
- """Return the device class of the device."""
- return DEVICE_CLASS_HUMIDIFIER
-
@property
def supported_features(self):
"""Return the list of supported features."""
@@ -140,6 +137,8 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity):
class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity):
"""Representation of a HomeKit Controller Humidifier."""
+ _attr_device_class = DEVICE_CLASS_DEHUMIDIFIER
+
def get_characteristic_types(self):
"""Define the homekit characteristics the entity cares about."""
return [
@@ -151,11 +150,6 @@ class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity):
CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD,
]
- @property
- def device_class(self) -> str:
- """Return the device class of the device."""
- return DEVICE_CLASS_DEHUMIDIFIER
-
@property
def supported_features(self):
"""Return the list of supported features."""
diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json
index 7ff32e402fe..496d629d112 100644
--- a/homeassistant/components/homekit_controller/manifest.json
+++ b/homeassistant/components/homekit_controller/manifest.json
@@ -3,7 +3,7 @@
"name": "HomeKit Controller",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
- "requirements": ["aiohomekit==0.2.67"],
+ "requirements": ["aiohomekit==0.4.2"],
"zeroconf": ["_hap._tcp.local."],
"after_dependencies": ["zeroconf"],
"codeowners": ["@Jc2k", "@bdraco"],
diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py
index 71bde5f0af9..1134e4bb4da 100644
--- a/homeassistant/components/homekit_controller/media_player.py
+++ b/homeassistant/components/homekit_controller/media_player.py
@@ -57,6 +57,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
"""Representation of a HomeKit Controller Television."""
+ _attr_device_class = DEVICE_CLASS_TV
+
def get_characteristic_types(self):
"""Define the homekit characteristics the entity cares about."""
return [
@@ -70,11 +72,6 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
CharacteristicsTypes.IDENTIFIER,
]
- @property
- def device_class(self):
- """Define the device class for a HomeKit enabled TV."""
- return DEVICE_CLASS_TV
-
@property
def supported_features(self):
"""Flag media player features that are supported."""
diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py
index 2ae264fabb9..fe98b75130c 100644
--- a/homeassistant/components/homekit_controller/sensor.py
+++ b/homeassistant/components/homekit_controller/sensor.py
@@ -1,8 +1,8 @@
"""Support for Homekit sensors."""
-from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
-from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_BATTERY,
@@ -28,14 +28,23 @@ SIMPLE_SENSOR = {
CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: {
"name": "Real Time Energy",
"device_class": DEVICE_CLASS_POWER,
+ "state_class": STATE_CLASS_MEASUREMENT,
"unit": "watts",
- "icon": "mdi:chart-line",
},
CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: {
"name": "Real Time Energy",
"device_class": DEVICE_CLASS_POWER,
+ "state_class": STATE_CLASS_MEASUREMENT,
"unit": "watts",
- "icon": "mdi:chart-line",
+ },
+ CharacteristicsTypes.get_uuid(CharacteristicsTypes.TEMPERATURE_CURRENT): {
+ "name": "Current Temperature",
+ "device_class": DEVICE_CLASS_TEMPERATURE,
+ "state_class": STATE_CLASS_MEASUREMENT,
+ "unit": TEMP_CELSIUS,
+ # This sensor is only for temperature characteristics that are not part
+ # of a temperature sensor service.
+ "probe": lambda char: char.service.type != ServicesTypes.TEMPERATURE_SENSOR,
},
}
@@ -43,15 +52,13 @@ SIMPLE_SENSOR = {
class HomeKitHumiditySensor(HomeKitEntity, SensorEntity):
"""Representation of a Homekit humidity sensor."""
+ _attr_device_class = DEVICE_CLASS_HUMIDITY
+ _attr_unit_of_measurement = PERCENTAGE
+
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT]
- @property
- def device_class(self) -> str:
- """Return the device class of the sensor."""
- return DEVICE_CLASS_HUMIDITY
-
@property
def name(self):
"""Return the name of the device."""
@@ -62,11 +69,6 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity):
"""Return the sensor icon."""
return HUMIDITY_ICON
- @property
- def unit_of_measurement(self):
- """Return units for the sensor."""
- return PERCENTAGE
-
@property
def state(self):
"""Return the current humidity."""
@@ -76,15 +78,13 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity):
class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity):
"""Representation of a Homekit temperature sensor."""
+ _attr_device_class = DEVICE_CLASS_TEMPERATURE
+ _attr_unit_of_measurement = TEMP_CELSIUS
+
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.TEMPERATURE_CURRENT]
- @property
- def device_class(self) -> str:
- """Return the device class of the sensor."""
- return DEVICE_CLASS_TEMPERATURE
-
@property
def name(self):
"""Return the name of the device."""
@@ -95,11 +95,6 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity):
"""Return the sensor icon."""
return TEMP_C_ICON
- @property
- def unit_of_measurement(self):
- """Return units for the sensor."""
- return TEMP_CELSIUS
-
@property
def state(self):
"""Return the current temperature in Celsius."""
@@ -109,15 +104,13 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity):
class HomeKitLightSensor(HomeKitEntity, SensorEntity):
"""Representation of a Homekit light level sensor."""
+ _attr_device_class = DEVICE_CLASS_ILLUMINANCE
+ _attr_unit_of_measurement = LIGHT_LUX
+
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.LIGHT_LEVEL_CURRENT]
- @property
- def device_class(self) -> str:
- """Return the device class of the sensor."""
- return DEVICE_CLASS_ILLUMINANCE
-
@property
def name(self):
"""Return the name of the device."""
@@ -128,11 +121,6 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity):
"""Return the sensor icon."""
return BRIGHTNESS_ICON
- @property
- def unit_of_measurement(self):
- """Return units for the sensor."""
- return LIGHT_LUX
-
@property
def state(self):
"""Return the current light level in lux."""
@@ -142,6 +130,9 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity):
class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity):
"""Representation of a Homekit Carbon Dioxide sensor."""
+ _attr_icon = CO2_ICON
+ _attr_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
+
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.CARBON_DIOXIDE_LEVEL]
@@ -151,16 +142,6 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity):
"""Return the name of the device."""
return f"{super().name} CO2"
- @property
- def icon(self):
- """Return the sensor icon."""
- return CO2_ICON
-
- @property
- def unit_of_measurement(self):
- """Return units for the sensor."""
- return CONCENTRATION_PARTS_PER_MILLION
-
@property
def state(self):
"""Return the current CO2 level in ppm."""
@@ -170,6 +151,9 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity):
class HomeKitBatterySensor(HomeKitEntity, SensorEntity):
"""Representation of a Homekit battery sensor."""
+ _attr_device_class = DEVICE_CLASS_BATTERY
+ _attr_unit_of_measurement = PERCENTAGE
+
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
return [
@@ -178,11 +162,6 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity):
CharacteristicsTypes.CHARGING_STATE,
]
- @property
- def device_class(self) -> str:
- """Return the device class of the sensor."""
- return DEVICE_CLASS_BATTERY
-
@property
def name(self):
"""Return the name of the device."""
@@ -210,11 +189,6 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity):
return icon
- @property
- def unit_of_measurement(self):
- """Return units for the sensor."""
- return PERCENTAGE
-
@property
def is_low_battery(self):
"""Return true if battery level is low."""
@@ -251,12 +225,15 @@ class SimpleSensor(CharacteristicEntity, SensorEntity):
info,
char,
device_class=None,
+ state_class=None,
unit=None,
icon=None,
name=None,
+ **kwargs,
):
"""Initialise a secondary HomeKit characteristic sensor."""
self._device_class = device_class
+ self._state_class = state_class
self._unit = unit
self._icon = icon
self._name = name
@@ -270,9 +247,14 @@ class SimpleSensor(CharacteristicEntity, SensorEntity):
@property
def device_class(self):
- """Return units for the sensor."""
+ """Return type of sensor."""
return self._device_class
+ @property
+ def state_class(self):
+ """Return type of state."""
+ return self._state_class
+
@property
def unit_of_measurement(self):
"""Return units for the sensor."""
@@ -320,10 +302,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
conn.add_listener(async_add_service)
@callback
- def async_add_characteristic(char):
+ def async_add_characteristic(char: Characteristic):
kwargs = SIMPLE_SENSOR.get(char.type)
if not kwargs:
return False
+ if "probe" in kwargs and not kwargs["probe"](char):
+ return False
info = {"aid": char.service.accessory.aid, "iid": char.service.iid}
async_add_entities([SimpleSensor(conn, info, char, **kwargs)], True)
diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json
index e46e083f1bd..ff9f180c943 100644
--- a/homeassistant/components/homekit_controller/translations/ca.json
+++ b/homeassistant/components/homekit_controller/translations/ca.json
@@ -12,6 +12,7 @@
},
"error": {
"authentication_error": "Codi HomeKit incorrecte. Verifica'l i torna-ho a provar.",
+ "insecure_setup_code": "El codi de configuraci\u00f3 sol\u00b7licitat no \u00e9s segur per naturalesa. Aquest accessori no compleix els requisits b\u00e0sics de seguretat.",
"max_peers_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 no t\u00e9 suficient espai lliure.",
"pairing_failed": "S'ha produ\u00eft un error mentre s'intentava la vinculaci\u00f3 amb aquest dispositiu. Pot ser que sigui un error temporal o pot ser que el teu dispositiu encara no sigui compatible.",
"unable_to_pair": "No s'ha pogut vincular, torna-ho a provar.",
@@ -29,6 +30,7 @@
},
"pair": {
"data": {
+ "allow_insecure_setup_codes": "Permet la vinculaci\u00f3 amb codis de configuraci\u00f3 insegurs.",
"pairing_code": "Codi de vinculaci\u00f3"
},
"description": "El controlador HomeKit es comunica amb {name} a trav\u00e9s de la xarxa d'\u00e0rea local utilitzant una connexi\u00f3 segura encriptada sense un HomeKit o iCloud separats. Introdueix el codi de vinculaci\u00f3 de HomeKit (en format XXX-XX-XXX) per utilitzar aquest accessori. Aquest codi es troba normalment en el propi dispositiu o en la seva caixa.",
diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json
index 120e9a63e66..7df1d0fc1a7 100644
--- a/homeassistant/components/homekit_controller/translations/de.json
+++ b/homeassistant/components/homekit_controller/translations/de.json
@@ -12,12 +12,13 @@
},
"error": {
"authentication_error": "Ung\u00fcltiger HomeKit Code, \u00fcberpr\u00fcfe bitte den Code und versuche es erneut.",
+ "insecure_setup_code": "Der angeforderte Setup-Code ist unsicher, da er zu trivial ist. Dieses Zubeh\u00f6r erf\u00fcllt nicht die grundlegenden Sicherheitsanforderungen.",
"max_peers_error": "Das Ger\u00e4t weigerte sich, die Kopplung durchzuf\u00fchren, da es keinen freien Kopplungs-Speicher hat.",
"pairing_failed": "Beim Versuch dieses Ger\u00e4t zu koppeln ist ein Fehler aufgetreten. Dies kann ein vor\u00fcbergehender Fehler sein oder das Ger\u00e4t wird derzeit m\u00f6glicherweise nicht unterst\u00fctzt.",
"unable_to_pair": "Koppeln fehltgeschlagen, bitte versuche es erneut",
"unknown_error": "Das Ger\u00e4t meldete einen unbekannten Fehler. Die Kopplung ist fehlgeschlagen."
},
- "flow_title": "HomeKit-Zubeh\u00f6r: {name}",
+ "flow_title": "{name}",
"step": {
"busy_error": {
"description": "Brechen Sie das Pairing auf allen Controllern ab oder versuchen Sie, das Ger\u00e4t neu zu starten, und fahren Sie dann fort, das Pairing fortzusetzen.",
@@ -29,19 +30,21 @@
},
"pair": {
"data": {
+ "allow_insecure_setup_codes": "Pairing mit unsicheren Setup-Codes zulassen.",
"pairing_code": "Kopplungscode"
},
- "description": "Gib deinen HomeKit-Kopplungscode ein, um dieses Zubeh\u00f6r zu verwenden",
+ "description": "HomeKit Controller kommuniziert mit {name} \u00fcber das lokale Netzwerk mit einer sicheren verschl\u00fcsselten Verbindung ohne separaten HomeKit Controller oder iCloud. Geben Sie Ihren HomeKit-Kopplungscode (im Format XXX-XX-XXX) ein, um dieses Zubeh\u00f6r zu verwenden. Dieser Code befindet sich in der Regel auf dem Ger\u00e4t selbst oder in der Verpackung.",
"title": "Mit HomeKit Zubeh\u00f6r koppeln"
},
"protocol_error": {
+ "description": "Das Ger\u00e4t befindet sich m\u00f6glicherweise nicht im Pairing-Modus und erfordert einen physischen oder virtuellen Tastendruck. Stellen Sie sicher, dass sich das Ger\u00e4t im Pairing-Modus befindet, oder versuchen Sie, das Ger\u00e4t neu zu starten und fahren Sie dann das Pairing fort.",
"title": "Fehler bei der Kommunikation mit dem Zubeh\u00f6r"
},
"user": {
"data": {
"device": "Ger\u00e4t"
},
- "description": "W\u00e4hle das Ger\u00e4t aus, mit dem du die Kopplung herstellen m\u00f6chtest",
+ "description": "HomeKit Controller kommuniziert \u00fcber das lokale Netzwerk mit einer sicheren verschl\u00fcsselten Verbindung ohne separaten HomeKit Controller oder iCloud. W\u00e4hle das Ger\u00e4t aus, mit dem du die Kopplung herstellen m\u00f6chtest",
"title": "Ger\u00e4teauswahl"
}
}
diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json
index 9f5f40bd199..52b295ecf21 100644
--- a/homeassistant/components/homekit_controller/translations/es.json
+++ b/homeassistant/components/homekit_controller/translations/es.json
@@ -12,6 +12,7 @@
},
"error": {
"authentication_error": "C\u00f3digo HomeKit incorrecto. Por favor, compru\u00e9belo e int\u00e9ntelo de nuevo.",
+ "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado es inseguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.",
"max_peers_error": "El dispositivo rechaz\u00f3 el emparejamiento ya que no tiene almacenamiento de emparejamientos libres.",
"pairing_failed": "Se ha producido un error no controlado al intentar emparejarse con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no est\u00e9 admitido en este momento.",
"unable_to_pair": "No se ha podido emparejar, por favor int\u00e9ntelo de nuevo.",
@@ -29,6 +30,7 @@
},
"pair": {
"data": {
+ "allow_insecure_setup_codes": "Permitir el emparejamiento con c\u00f3digos de configuraci\u00f3n inseguros.",
"pairing_code": "C\u00f3digo de vinculaci\u00f3n"
},
"description": "El controlador de HomeKit se comunica con {name} a trav\u00e9s de la red de \u00e1rea local usando una conexi\u00f3n encriptada segura sin un controlador HomeKit separado o iCloud. Introduce el c\u00f3digo de vinculaci\u00f3n de tu HomeKit (con el formato XXX-XX-XXX) para usar este accesorio. Este c\u00f3digo suele encontrarse en el propio dispositivo o en el embalaje.",
diff --git a/homeassistant/components/homekit_controller/translations/he.json b/homeassistant/components/homekit_controller/translations/he.json
new file mode 100644
index 00000000000..1028351a1bc
--- /dev/null
+++ b/homeassistant/components/homekit_controller/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea"
+ },
+ "flow_title": "{name}"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/nl.json b/homeassistant/components/homekit_controller/translations/nl.json
index 64b1d0802db..4312fdc033c 100644
--- a/homeassistant/components/homekit_controller/translations/nl.json
+++ b/homeassistant/components/homekit_controller/translations/nl.json
@@ -12,6 +12,7 @@
},
"error": {
"authentication_error": "Onjuiste HomeKit-code. Controleer het en probeer het opnieuw.",
+ "insecure_setup_code": "De gevraagde setup-code is onveilig vanwege de triviale aard ervan. Dit accessoire voldoet niet aan de basisbeveiligingsvereisten.",
"max_peers_error": "Apparaat heeft geweigerd om koppelingen toe te voegen omdat het geen vrije koppelingsopslag heeft.",
"pairing_failed": "Er deed zich een fout voor tijdens het koppelen met dit apparaat. Dit kan een tijdelijke storing zijn of uw apparaat wordt mogelijk momenteel niet ondersteund.",
"unable_to_pair": "Kan niet koppelen, probeer het opnieuw.",
@@ -29,6 +30,7 @@
},
"pair": {
"data": {
+ "allow_insecure_setup_codes": "Koppelen met onveilige setup-codes toestaan.",
"pairing_code": "Koppelingscode"
},
"description": "HomeKit Controller communiceert met {name} via het lokale netwerk met behulp van een beveiligde versleutelde verbinding zonder een aparte HomeKit-controller of iCloud. Voer uw HomeKit-koppelcode in (in de indeling XXX-XX-XXX) om dit accessoire te gebruiken. Deze code is meestal te vinden op het apparaat zelf of in de verpakking.",
diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json
index 87a7d4ef2b5..d7b5cc69cf3 100644
--- a/homeassistant/components/homekit_controller/translations/pl.json
+++ b/homeassistant/components/homekit_controller/translations/pl.json
@@ -12,6 +12,7 @@
},
"error": {
"authentication_error": "Niepoprawny kod parowania HomeKit. Sprawd\u017a go i spr\u00f3buj ponownie.",
+ "insecure_setup_code": "\u017b\u0105dany kod instalacyjny jest niezabezpieczony ze wzgl\u0119du na jego trywialny charakter. To akcesorium nie spe\u0142nia podstawowych wymaga\u0144 bezpiecze\u0144stwa.",
"max_peers_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c nie ma wolnej pami\u0119ci parowania",
"pairing_failed": "Wyst\u0105pi\u0142 nieobs\u0142ugiwany b\u0142\u0105d podczas pr\u00f3by sparowania z tym urz\u0105dzeniem. Mo\u017ce to by\u0107 tymczasowa awaria lub urz\u0105dzenie mo\u017ce nie by\u0107 obecnie obs\u0142ugiwane.",
"unable_to_pair": "Nie mo\u017cna sparowa\u0107, spr\u00f3buj ponownie",
@@ -29,6 +30,7 @@
},
"pair": {
"data": {
+ "allow_insecure_setup_codes": "Zezwalaj na parowanie z niezabezpieczonymi kodami konfiguracji.",
"pairing_code": "Kod parowania"
},
"description": "Kontroler HomeKit komunikuje si\u0119 z {name} poprzez sie\u0107 lokaln\u0105 za pomoc\u0105 bezpiecznego, szyfrowanego po\u0142\u0105czenia bez oddzielnego kontrolera HomeKit lub iCloud. Wprowad\u017a kod parowania (w formacie XXX-XX-XXX), aby u\u017cy\u0107 tego akcesorium. Ten kod zazwyczaj znajduje si\u0119 na samym urz\u0105dzeniu lub w jego opakowaniu.",
diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py
index 286c7372fd2..c57e4cd15c7 100644
--- a/homeassistant/components/homematic/binary_sensor.py
+++ b/homeassistant/components/homematic/binary_sensor.py
@@ -74,10 +74,7 @@ class HMBinarySensor(HMDevice, BinarySensorEntity):
class HMBatterySensor(HMDevice, BinarySensorEntity):
"""Representation of an HomeMatic low battery sensor."""
- @property
- def device_class(self):
- """Return battery as a device class."""
- return DEVICE_CLASS_BATTERY
+ _attr_device_class = DEVICE_CLASS_BATTERY
@property
def is_on(self):
diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py
index 864441c2aa6..4f1c1d12f81 100644
--- a/homeassistant/components/homematic/const.py
+++ b/homeassistant/components/homematic/const.py
@@ -61,6 +61,7 @@ HM_DEVICE_TYPES = {
"IOSwitchWireless",
"IPWIODevice",
"IPSwitchBattery",
+ "IPMultiIOPCB",
],
DISCOVER_LIGHTS: [
"Dimmer",
@@ -122,6 +123,8 @@ HM_DEVICE_TYPES = {
"IPKeyBlindTilt",
"IPLanRouter",
"TempModuleSTE2",
+ "IPMultiIOPCB",
+ "ValveBoxW",
],
DISCOVER_CLIMATE: [
"Thermostat",
@@ -134,6 +137,7 @@ HM_DEVICE_TYPES = {
"ThermostatGroup",
"IPThermostatWall230V",
"IPThermostatWall2",
+ "IPWThermostatWall",
],
DISCOVER_BINARY_SENSORS: [
"ShutterContact",
@@ -167,6 +171,7 @@ HM_DEVICE_TYPES = {
"IPAlarmSensor",
"IPRainSensor",
"IPLanRouter",
+ "IPMultiIOPCB",
],
DISCOVER_COVER: [
"Blind",
diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py
index e9f2943b53b..deed671931f 100644
--- a/homeassistant/components/homematic/cover.py
+++ b/homeassistant/components/homematic/cover.py
@@ -112,6 +112,8 @@ class HMCover(HMDevice, CoverEntity):
class HMGarage(HMCover):
"""Represents a Homematic Garage cover. Homematic garage covers do not support position attributes."""
+ _attr_device_class = DEVICE_CLASS_GARAGE
+
@property
def current_cover_position(self):
"""
@@ -127,11 +129,6 @@ class HMGarage(HMCover):
"""Return whether the cover is closed."""
return self._hmdevice.is_closed(self._hm_get_state())
- @property
- def device_class(self):
- """Return the device class."""
- return DEVICE_CLASS_GARAGE
-
def _init_data_struct(self):
"""Generate a data dictionary (self._data) from metadata."""
self._state = "DOOR_STATE"
diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json
index ce192bc3808..8b1ee62a09e 100644
--- a/homeassistant/components/homematic/manifest.json
+++ b/homeassistant/components/homematic/manifest.json
@@ -2,7 +2,7 @@
"domain": "homematic",
"name": "Homematic",
"documentation": "https://www.home-assistant.io/integrations/homematic",
- "requirements": ["pyhomematic==0.1.72"],
+ "requirements": ["pyhomematic==0.1.73"],
"codeowners": ["@pvizeli", "@danielperna84"],
"iot_class": "local_push"
}
diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py
index 964ba15cd0a..62f2f0ccdff 100644
--- a/homeassistant/components/homematic/sensor.py
+++ b/homeassistant/components/homematic/sensor.py
@@ -52,6 +52,8 @@ HM_UNIT_HA_CAST = {
"ENERGY_COUNTER": ENERGY_WATT_HOUR,
"GAS_POWER": VOLUME_CUBIC_METERS,
"GAS_ENERGY_COUNTER": VOLUME_CUBIC_METERS,
+ "IEC_POWER": POWER_WATT,
+ "IEC_ENERGY_COUNTER": ENERGY_WATT_HOUR,
"LUX": LIGHT_LUX,
"ILLUMINATION": LIGHT_LUX,
"CURRENT_ILLUMINATION": LIGHT_LUX,
diff --git a/homeassistant/components/homematicip_cloud/translations/he.json b/homeassistant/components/homematicip_cloud/translations/he.json
index f07db79a1c5..b6a75868d3b 100644
--- a/homeassistant/components/homematicip_cloud/translations/he.json
+++ b/homeassistant/components/homematicip_cloud/translations/he.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\u05e0\u05e7\u05d5\u05d3\u05ea \u05d4\u05d2\u05d9\u05e9\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea",
- "connection_aborted": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e9\u05e8\u05ea HMIP",
- "unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4."
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "connection_aborted": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
},
"error": {
- "invalid_sgtin_or_pin": "PIN \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.",
+ "invalid_sgtin_or_pin": "SGTIN \u05d0\u05d5 \u05e7\u05d5\u05d3 PIN \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd, \u05e0\u05d0 \u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.",
"press_the_button": "\u05dc\u05d7\u05e5 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05db\u05d7\u05d5\u05dc.",
"register_failed": "\u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05e0\u05db\u05e9\u05dc, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.",
"timeout_button": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05db\u05d7\u05d5\u05dc, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1"
@@ -15,8 +15,8 @@
"init": {
"data": {
"hapid": "\u05de\u05d6\u05d4\u05d4 \u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 (SGTIN)",
- "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9, \u05de\u05e9\u05de\u05e9 \u05db\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05e2\u05d1\u05d5\u05e8 \u05db\u05dc \u05d4\u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd)",
- "pin": "\u05e7\u05d5\u05d3 PIN (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)"
+ "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9, \u05de\u05e9\u05de\u05e9 \u05db\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05e2\u05d1\u05d5\u05e8 \u05db\u05dc \u05d4\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd)",
+ "pin": "\u05e7\u05d5\u05d3 PIN"
},
"title": "\u05d1\u05d7\u05e8 \u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 HomematicIP"
},
diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py
index 5c62a469924..18bc51af1d1 100644
--- a/homeassistant/components/http/forwarded.py
+++ b/homeassistant/components/http/forwarded.py
@@ -47,7 +47,8 @@ def async_setup_forwarded(
Additionally:
- If no X-Forwarded-For header is found, the processing of all headers is skipped.
- - Log a warning when untrusted connected peer provides X-Forwarded-For headers.
+ - Throw HTTP 400 status when untrusted connected peer provides
+ X-Forwarded-For headers.
- If multiple instances of X-Forwarded-For, X-Forwarded-Proto or
X-Forwarded-Host are found, an HTTP 400 status code is thrown.
- If malformed or invalid (IP) data in X-Forwarded-For header is found,
@@ -87,26 +88,20 @@ def async_setup_forwarded(
# We have X-Forwarded-For, but config does not agree
if not use_x_forwarded_for:
- _LOGGER.warning(
+ _LOGGER.error(
"A request from a reverse proxy was received from %s, but your "
- "HTTP integration is not set-up for reverse proxies; "
- "This request will be blocked in Home Assistant 2021.7 unless "
- "you configure your HTTP integration to allow this header",
+ "HTTP integration is not set-up for reverse proxies",
connected_ip,
)
- # Block this request in the future, for now we pass.
- return await handler(request)
+ raise HTTPBadRequest
# Ensure the IP of the connected peer is trusted
if not any(connected_ip in trusted_proxy for trusted_proxy in trusted_proxies):
- _LOGGER.warning(
- "Received X-Forwarded-For header from untrusted proxy %s, headers not processed; "
- "This request will be blocked in Home Assistant 2021.7 unless you configure "
- "your HTTP integration to allow this proxy to reverse your Home Assistant instance",
+ _LOGGER.error(
+ "Received X-Forwarded-For header from an untrusted proxy %s",
connected_ip,
)
- # Not trusted, Block this request in the future, continue as normal
- return await handler(request)
+ raise HTTPBadRequest
# Multiple X-Forwarded-For headers
if len(forwarded_for_headers) > 1:
diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py
index ebb54ab75c6..7503c1d5e71 100644
--- a/homeassistant/components/huawei_lte/__init__.py
+++ b/homeassistant/components/huawei_lte/__init__.py
@@ -304,9 +304,9 @@ class HuaweiLteData:
routers: dict[str, Router] = attr.ib(init=False, factory=dict)
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Huawei LTE component from config entry."""
- url = config_entry.data[CONF_URL]
+ 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
@@ -317,30 +317,29 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
for key in CONF_USERNAME, CONF_PASSWORD:
if key in yaml_config:
value = yaml_config[key]
- if value != config_entry.data.get(f"{key}_from_yaml"):
+ 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 != config_entry.options.get(
+ 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 != config_entry.options.get(f"{CONF_NAME}_from_yaml")
+ 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(
- config_entry,
- data={**config_entry.data, **new_data},
- options={**config_entry.options, **new_options},
+ entry,
+ data={**entry.data, **new_data},
+ options={**entry.options, **new_options},
)
# Get MAC address for use in unique ids. Being able to use something
@@ -363,8 +362,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
Authorized one if username/pass specified (even if empty), unauthorized one otherwise.
"""
- username = config_entry.data.get(CONF_USERNAME)
- password = config_entry.data.get(CONF_PASSWORD)
+ username = entry.data.get(CONF_USERNAME)
+ password = entry.data.get(CONF_PASSWORD)
if username or password:
connection: Connection = AuthorizedConnection(
url, username=username, password=password, timeout=CONNECTION_TIMEOUT
@@ -383,7 +382,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
raise ConfigEntryNotReady from ex
# Set up router and store reference to it
- router = Router(config_entry, connection, url, mac, signal_update)
+ router = Router(entry, connection, url, mac, signal_update)
hass.data[DOMAIN].routers[url] = router
# Do initial data update
@@ -409,7 +408,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
device_data["sw_version"] = sw_version
device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create(
- config_entry_id=config_entry.entry_id,
+ config_entry_id=entry.entry_id,
connections=router.device_connections,
identifiers=router.device_identifiers,
name=router.device_name,
@@ -418,7 +417,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
)
# Forward config entry setup to platforms
- hass.config_entries.async_setup_platforms(config_entry, CONFIG_ENTRY_PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, CONFIG_ENTRY_PLATFORMS)
# Notify doesn't support config entry setup yet, load with discovery for now
await discovery.async_load_platform(
@@ -427,8 +426,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
DOMAIN,
{
CONF_URL: url,
- CONF_NAME: config_entry.options.get(CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME),
- CONF_RECIPIENT: config_entry.options.get(CONF_RECIPIENT),
+ CONF_NAME: entry.options.get(CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME),
+ CONF_RECIPIENT: entry.options.get(CONF_RECIPIENT),
},
hass.data[DOMAIN].hass_config,
)
@@ -442,12 +441,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
router.update()
# Set up periodic update
- config_entry.async_on_unload(
+ entry.async_on_unload(
async_track_time_interval(hass, _update_router, SCAN_INTERVAL)
)
# Clean up at end
- config_entry.async_on_unload(
+ entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, router.cleanup)
)
diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json
index 10a7af41a3c..a979eeb89fe 100644
--- a/homeassistant/components/huawei_lte/translations/de.json
+++ b/homeassistant/components/huawei_lte/translations/de.json
@@ -15,7 +15,7 @@
"response_error": "Unbekannter Fehler vom Ger\u00e4t",
"unknown": "Unerwarteter Fehler"
},
- "flow_title": "Huawei LTE: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/huawei_lte/translations/he.json b/homeassistant/components/huawei_lte/translations/he.json
index 6f4191da70d..f55b325d867 100644
--- a/homeassistant/components/huawei_lte/translations/he.json
+++ b/homeassistant/components/huawei_lte/translations/he.json
@@ -1,10 +1,24 @@
{
"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"
+ },
+ "error": {
+ "incorrect_password": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4",
+ "incorrect_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05d2\u05d5\u05d9",
+ "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}",
"step": {
"user": {
"data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
- }
+ },
+ "description": "\u05d4\u05d6\u05df \u05e4\u05e8\u05d8\u05d9 \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d4\u05ea\u05e7\u05df. \u05e6\u05d9\u05d5\u05df \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d5\u05d0 \u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9, \u05d0\u05da \u05de\u05d0\u05e4\u05e9\u05e8 \u05ea\u05de\u05d9\u05db\u05d4 \u05d1\u05ea\u05db\u05d5\u05e0\u05d5\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 \u05e0\u05d5\u05e1\u05e4\u05d5\u05ea. \u05de\u05e6\u05d3 \u05e9\u05e0\u05d9, \u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d7\u05d9\u05d1\u05d5\u05e8 \u05de\u05d5\u05e8\u05e9\u05d4 \u05e2\u05dc\u05d5\u05dc \u05dc\u05d2\u05e8\u05d5\u05dd \u05dc\u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05d2\u05d9\u05e9\u05d4 \u05dc\u05de\u05de\u05e9\u05e7 \u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df \u05de\u05d7\u05d5\u05e5 \u05dc-Home Assistant \u05d1\u05d6\u05de\u05df \u05e9\u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05e4\u05e2\u05d9\u05dc, \u05d5\u05dc\u05d4\u05d9\u05e4\u05da."
}
}
}
diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json
index 815794133d2..5c045d0c6f3 100644
--- a/homeassistant/components/huawei_lte/translations/hu.json
+++ b/homeassistant/components/huawei_lte/translations/hu.json
@@ -13,7 +13,7 @@
"invalid_url": "\u00c9rv\u00e9nytelen URL",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "Huawei LTE: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py
index e65b362f9e7..ea91cd07d8c 100644
--- a/homeassistant/components/hue/device_trigger.py
+++ b/homeassistant/components/hue/device_trigger.py
@@ -1,7 +1,7 @@
"""Provides device automations for Philips Hue events."""
import voluptuous as vol
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.device_automation.exceptions import (
InvalidDeviceAutomationConfig,
)
@@ -55,6 +55,12 @@ HUE_BUTTON_REMOTE = {
(CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003},
}
+HUE_WALL_REMOTE_MODEL = "Hue wall switch module" # ZLLSWITCH/RDM001
+HUE_WALL_REMOTE = {
+ (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2002},
+}
+
HUE_TAP_REMOTE_MODEL = "Hue tap switch" # ZGPSWITCH
HUE_TAP_REMOTE = {
(CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 34},
@@ -84,10 +90,11 @@ REMOTES = {
HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE,
HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE,
HUE_BUTTON_REMOTE_MODEL: HUE_BUTTON_REMOTE,
+ HUE_WALL_REMOTE_MODEL: HUE_WALL_REMOTE,
HUE_FOHSWITCH_REMOTE_MODEL: HUE_FOHSWITCH_REMOTE,
}
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str}
)
diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json
index b61635cb408..3c8078364ab 100644
--- a/homeassistant/components/hue/manifest.json
+++ b/homeassistant/components/hue/manifest.json
@@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
- "requirements": ["aiohue==2.5.0"],
+ "requirements": ["aiohue==2.5.1"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",
diff --git a/homeassistant/components/hue/translations/he.json b/homeassistant/components/hue/translations/he.json
index 3b03fb84bc3..c014b0a52ae 100644
--- a/homeassistant/components/hue/translations/he.json
+++ b/homeassistant/components/hue/translations/he.json
@@ -3,6 +3,7 @@
"abort": {
"all_configured": "\u05db\u05dc \u05d4\u05de\u05d2\u05e9\u05e8\u05d9\u05dd \u05e9\u05dc Philips Hue \u05de\u05d5\u05d2\u05d3\u05e8\u05d9\u05dd \u05db\u05d1\u05e8",
"already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
+ "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea",
"cannot_connect": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05d2\u05e9\u05e8",
"discover_timeout": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d2\u05dc\u05d5\u05ea \u05de\u05d2\u05e9\u05e8\u05d9\u05dd",
"no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 Philips Hue",
@@ -22,7 +23,18 @@
"link": {
"description": "\u05dc\u05d7\u05e5 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05e2\u05dc \u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d3\u05d9 \u05dc\u05d7\u05d1\u05e8 \u05d1\u05d9\u05df \u05d0\u05ea Philips Hue \u05e2\u05dd Home Assistant. \n\n",
"title": "\u05e7\u05d9\u05e9\u05d5\u05e8 \u05dc\u05e8\u05db\u05d6\u05ea"
+ },
+ "manual": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "turn_off": "\u05db\u05d1\u05d4",
+ "turn_on": "\u05d4\u05e4\u05e2\u05dc"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py
index f89c9f07625..8bd07474705 100644
--- a/homeassistant/components/huisbaasje/__init__.py
+++ b/homeassistant/components/huisbaasje/__init__.py
@@ -28,7 +28,7 @@ PLATFORMS = ["sensor"]
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Huisbaasje from a config entry."""
# Create the Huisbaasje client
huisbaasje = Huisbaasje(
diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py
index abac03e6182..f2565a15ce2 100644
--- a/homeassistant/components/huisbaasje/const.py
+++ b/homeassistant/components/huisbaasje/const.py
@@ -8,6 +8,7 @@ from huisbaasje.const import (
SOURCE_TYPE_GAS,
)
+from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
from homeassistant.const import (
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
@@ -48,26 +49,31 @@ SENSORS_INFO = [
"name": "Huisbaasje Current Power",
"device_class": DEVICE_CLASS_POWER,
"source_type": SOURCE_TYPE_ELECTRICITY,
+ "state_class": STATE_CLASS_MEASUREMENT,
},
{
"name": "Huisbaasje Current Power In",
"device_class": DEVICE_CLASS_POWER,
"source_type": SOURCE_TYPE_ELECTRICITY_IN,
+ "state_class": STATE_CLASS_MEASUREMENT,
},
{
"name": "Huisbaasje Current Power In Low",
"device_class": DEVICE_CLASS_POWER,
"source_type": SOURCE_TYPE_ELECTRICITY_IN_LOW,
+ "state_class": STATE_CLASS_MEASUREMENT,
},
{
"name": "Huisbaasje Current Power Out",
"device_class": DEVICE_CLASS_POWER,
"source_type": SOURCE_TYPE_ELECTRICITY_OUT,
+ "state_class": STATE_CLASS_MEASUREMENT,
},
{
"name": "Huisbaasje Current Power Out Low",
"device_class": DEVICE_CLASS_POWER,
"source_type": SOURCE_TYPE_ELECTRICITY_OUT_LOW,
+ "state_class": STATE_CLASS_MEASUREMENT,
},
{
"name": "Huisbaasje Energy Today",
@@ -107,6 +113,7 @@ SENSORS_INFO = [
"source_type": SOURCE_TYPE_GAS,
"icon": "mdi:fire",
"precision": 1,
+ "state_class": STATE_CLASS_MEASUREMENT,
},
{
"name": "Huisbaasje Gas Today",
diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py
index 1ea392b8269..038325ece4a 100644
--- a/homeassistant/components/huisbaasje/sensor.py
+++ b/homeassistant/components/huisbaasje/sensor.py
@@ -1,4 +1,6 @@
"""Platform for sensor integration."""
+from __future__ import annotations
+
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, POWER_WATT
@@ -38,6 +40,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity):
unit_of_measurement: str = POWER_WATT,
icon: str = "mdi:lightning-bolt",
precision: int = 0,
+ state_class: str | None = None,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
@@ -49,6 +52,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity):
self._sensor_type = sensor_type
self._icon = icon
self._precision = precision
+ self._attr_state_class = state_class
@property
def unique_id(self) -> str:
diff --git a/homeassistant/components/huisbaasje/translations/he.json b/homeassistant/components/huisbaasje/translations/he.json
new file mode 100644
index 00000000000..c479d8488f2
--- /dev/null
+++ b/homeassistant/components/huisbaasje/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"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py
index 9500b74aba6..7839eeec799 100644
--- a/homeassistant/components/humidifier/__init__.py
+++ b/homeassistant/components/humidifier/__init__.py
@@ -91,17 +91,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(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."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class HumidifierEntity(ToggleEntity):
"""Base class for humidifier entities."""
+ _attr_available_modes: list[str] | None
+ _attr_max_humidity: int = DEFAULT_MAX_HUMIDITY
+ _attr_min_humidity: int = DEFAULT_MIN_HUMIDITY
+ _attr_mode: str | None
+ _attr_target_humidity: int | None = None
+
@property
def capability_attributes(self) -> dict[str, Any]:
"""Return capability attributes."""
@@ -134,7 +142,7 @@ class HumidifierEntity(ToggleEntity):
@property
def target_humidity(self) -> int | None:
"""Return the humidity we try to reach."""
- return None
+ return self._attr_target_humidity
@property
def mode(self) -> str | None:
@@ -142,7 +150,7 @@ class HumidifierEntity(ToggleEntity):
Requires SUPPORT_MODES.
"""
- raise NotImplementedError
+ return self._attr_mode
@property
def available_modes(self) -> list[str] | None:
@@ -150,7 +158,7 @@ class HumidifierEntity(ToggleEntity):
Requires SUPPORT_MODES.
"""
- raise NotImplementedError
+ return self._attr_available_modes
def set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
@@ -171,9 +179,9 @@ class HumidifierEntity(ToggleEntity):
@property
def min_humidity(self) -> int:
"""Return the minimum humidity."""
- return DEFAULT_MIN_HUMIDITY
+ return self._attr_min_humidity
@property
def max_humidity(self) -> int:
"""Return the maximum humidity."""
- return DEFAULT_MAX_HUMIDITY
+ return self._attr_max_humidity
diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py
index fa9c1eb71e7..81df6938236 100644
--- a/homeassistant/components/humidifier/device_action.py
+++ b/homeassistant/components/humidifier/device_action.py
@@ -7,15 +7,15 @@ from homeassistant.components.device_automation import toggle_entity
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
- ATTR_SUPPORTED_FEATURES,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_TYPE,
)
-from homeassistant.core import Context, HomeAssistant
+from homeassistant.core import Context, HomeAssistant, HomeAssistantError
from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import get_capability, get_supported_features
from . import DOMAIN, const
@@ -50,30 +50,17 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
if entry.domain != DOMAIN:
continue
- state = hass.states.get(entry.entity_id)
+ supported_features = get_supported_features(hass, entry.entity_id)
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "set_humidity",
- }
- )
+ base_action = {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+ actions.append({**base_action, CONF_TYPE: "set_humidity"})
- # We need a state or else we can't populate the available modes.
- if state is None:
- continue
-
- if state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_MODES:
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "set_mode",
- }
- )
+ if supported_features & const.SUPPORT_MODES:
+ actions.append({**base_action, CONF_TYPE: "set_mode"})
return actions
@@ -102,7 +89,6 @@ async def async_call_action_from_config(
async def async_get_action_capabilities(hass, config):
"""List action capabilities."""
- state = hass.states.get(config[CONF_ENTITY_ID])
action_type = config[CONF_TYPE]
fields = {}
@@ -110,9 +96,12 @@ async def async_get_action_capabilities(hass, config):
if action_type == "set_humidity":
fields[vol.Required(const.ATTR_HUMIDITY)] = vol.Coerce(int)
elif action_type == "set_mode":
- if state:
- available_modes = state.attributes.get(const.ATTR_AVAILABLE_MODES, [])
- else:
+ try:
+ available_modes = (
+ get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_AVAILABLE_MODES)
+ or []
+ )
+ except HomeAssistantError:
available_modes = []
fields[vol.Required(ATTR_MODE)] = vol.In(available_modes)
else:
diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py
index 02a667f2f68..f2bf032b195 100644
--- a/homeassistant/components/humidifier/device_condition.py
+++ b/homeassistant/components/humidifier/device_condition.py
@@ -7,16 +7,16 @@ from homeassistant.components.device_automation import toggle_entity
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
- ATTR_SUPPORTED_FEATURES,
CONF_CONDITION,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_TYPE,
)
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant, HomeAssistantError, callback
from homeassistant.helpers import condition, config_validation as cv, entity_registry
from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
+from homeassistant.helpers.entity import get_capability, get_supported_features
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN, const
@@ -48,9 +48,9 @@ async def async_get_conditions(
if entry.domain != DOMAIN:
continue
- state = hass.states.get(entry.entity_id)
+ supported_features = get_supported_features(hass, entry.entity_id)
- if state and state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_MODES:
+ if supported_features & const.SUPPORT_MODES:
conditions.append(
{
CONF_CONDITION: "device",
@@ -87,15 +87,17 @@ def async_condition_from_config(
async def async_get_condition_capabilities(hass, config):
"""List condition capabilities."""
- state = hass.states.get(config[CONF_ENTITY_ID])
condition_type = config[CONF_TYPE]
fields = {}
if condition_type == "is_mode":
- if state:
- modes = state.attributes.get(const.ATTR_AVAILABLE_MODES, [])
- else:
+ try:
+ modes = (
+ get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_AVAILABLE_MODES)
+ or []
+ )
+ except HomeAssistantError:
modes = []
fields[vol.Required(ATTR_MODE)] = vol.In(modes)
diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py
index d0f462f6b0f..98bbe192a1f 100644
--- a/homeassistant/components/humidifier/device_trigger.py
+++ b/homeassistant/components/humidifier/device_trigger.py
@@ -5,7 +5,7 @@ import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
from homeassistant.components.device_automation import (
- TRIGGER_BASE_SCHEMA,
+ DEVICE_TRIGGER_BASE_SCHEMA,
toggle_entity,
)
from homeassistant.components.homeassistant.triggers import (
@@ -29,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType
from . import DOMAIN
TARGET_TRIGGER_SCHEMA = vol.All(
- TRIGGER_BASE_SCHEMA.extend(
+ DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): "target_humidity_changed",
diff --git a/homeassistant/components/humidifier/translations/he.json b/homeassistant/components/humidifier/translations/he.json
new file mode 100644
index 00000000000..aa14b7d234f
--- /dev/null
+++ b/homeassistant/components/humidifier/translations/he.json
@@ -0,0 +1,25 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_mode": "\u05e9\u05e0\u05d4 \u05de\u05e6\u05d1 \u05d1-{entity_name}",
+ "toggle": "\u05d4\u05d7\u05dc\u05e3 \u05d0\u05ea {entity_name}",
+ "turn_off": "\u05db\u05d1\u05d4 \u05d0\u05ea {entity_name}",
+ "turn_on": "\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea {entity_name}"
+ },
+ "condition_type": {
+ "is_mode": "{entity_name} \u05de\u05d5\u05d2\u05d3\u05e8 \u05dc\u05de\u05e6\u05d1 \u05de\u05e1\u05d5\u05d9\u05dd",
+ "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9",
+ "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4",
+ "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc"
+ }
+ },
+ "state": {
+ "_": {
+ "off": "\u05db\u05d1\u05d5\u05d9",
+ "on": "\u05de\u05d5\u05e4\u05e2\u05dc"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py
index d9a52446028..7b945d9bdfe 100644
--- a/homeassistant/components/hunterdouglas_powerview/__init__.py
+++ b/homeassistant/components/hunterdouglas_powerview/__init__.py
@@ -59,7 +59,7 @@ PLATFORMS = ["cover", "scene", "sensor"]
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Hunter Douglas PowerView from a config entry."""
config = entry.data
diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py
index 58c7e90994c..901a048fc7f 100644
--- a/homeassistant/components/hunterdouglas_powerview/cover.py
+++ b/homeassistant/components/hunterdouglas_powerview/cover.py
@@ -91,9 +91,9 @@ def hd_position_to_hass(hd_position):
return round((hd_position / MAX_POSITION) * 100)
-def hass_position_to_hd(hass_positon):
+def hass_position_to_hd(hass_position):
"""Convert hass position to hunter douglas position."""
- return int(hass_positon / 100 * MAX_POSITION)
+ return int(hass_position / 100 * MAX_POSITION)
class PowerViewShade(ShadeEntity, CoverEntity):
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/de.json b/homeassistant/components/hunterdouglas_powerview/translations/de.json
index 4c843fb262f..db0fa18cc29 100644
--- a/homeassistant/components/hunterdouglas_powerview/translations/de.json
+++ b/homeassistant/components/hunterdouglas_powerview/translations/de.json
@@ -7,6 +7,7 @@
"cannot_connect": "Verbindung fehlgeschlagen",
"unknown": "Unerwarteter Fehler"
},
+ "flow_title": "{name} ({host})",
"step": {
"link": {
"description": "M\u00f6chten Sie {name} ({host}) einrichten?",
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/es.json b/homeassistant/components/hunterdouglas_powerview/translations/es.json
index 1095ed06ed7..a5cf5303000 100644
--- a/homeassistant/components/hunterdouglas_powerview/translations/es.json
+++ b/homeassistant/components/hunterdouglas_powerview/translations/es.json
@@ -7,6 +7,7 @@
"cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo",
"unknown": "Error inesperado"
},
+ "flow_title": "{name} ( {host} )",
"step": {
"link": {
"description": "\u00bfQuieres configurar {name} ({host})?",
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/he.json b/homeassistant/components/hunterdouglas_powerview/translations/he.json
index 39beabfa06e..c6610f79e77 100644
--- a/homeassistant/components/hunterdouglas_powerview/translations/he.json
+++ b/homeassistant/components/hunterdouglas_powerview/translations/he.json
@@ -1,6 +1,17 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "flow_title": "{name} ({host})",
"step": {
+ "link": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?"
+ },
"user": {
"data": {
"host": "\u05db\u05ea\u05d5\u05d1\u05ea IP"
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/hu.json b/homeassistant/components/hunterdouglas_powerview/translations/hu.json
index 063e0dad3c4..3de1b9d0117 100644
--- a/homeassistant/components/hunterdouglas_powerview/translations/hu.json
+++ b/homeassistant/components/hunterdouglas_powerview/translations/hu.json
@@ -7,6 +7,7 @@
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/pl.json b/homeassistant/components/hunterdouglas_powerview/translations/pl.json
index 1bf55f64477..fa8b1d856a2 100644
--- a/homeassistant/components/hunterdouglas_powerview/translations/pl.json
+++ b/homeassistant/components/hunterdouglas_powerview/translations/pl.json
@@ -7,6 +7,7 @@
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"unknown": "Nieoczekiwany b\u0142\u0105d"
},
+ "flow_title": "{name} ({host})",
"step": {
"link": {
"description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?",
diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py
index acdb3dcfb64..d57fe20f95e 100644
--- a/homeassistant/components/hvv_departures/__init__.py
+++ b/homeassistant/components/hvv_departures/__init__.py
@@ -13,7 +13,7 @@ from .hub import GTIHub
PLATFORMS = [DOMAIN_SENSOR, DOMAIN_BINARY_SENSOR]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HVV from a config entry."""
hub = GTIHub(
diff --git a/homeassistant/components/hvv_departures/translations/he.json b/homeassistant/components/hvv_departures/translations/he.json
index ac90b3264ea..19463e18610 100644
--- a/homeassistant/components/hvv_departures/translations/he.json
+++ b/homeassistant/components/hvv_departures/translations/he.json
@@ -1,12 +1,29 @@
{
"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",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
"step": {
"user": {
"data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "offset": "\u05d4\u05d9\u05e1\u05d8 (\u05d3\u05e7\u05d5\u05ea)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py
index ddadb4feea5..891f48e8738 100644
--- a/homeassistant/components/hyperion/__init__.py
+++ b/homeassistant/components/hyperion/__init__.py
@@ -9,6 +9,7 @@ from typing import Any, Callable, cast
from awesomeversion import AwesomeVersion
from hyperion import client, const as hyperion_const
+from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntry
@@ -34,7 +35,7 @@ from .const import (
SIGNAL_INSTANCE_REMOVE,
)
-PLATFORMS = [LIGHT_DOMAIN, SWITCH_DOMAIN]
+PLATFORMS = [LIGHT_DOMAIN, SWITCH_DOMAIN, CAMERA_DOMAIN]
_LOGGER = logging.getLogger(__name__)
@@ -136,11 +137,11 @@ def listen_for_instance_updates(
)
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Hyperion from a config entry."""
- host = config_entry.data[CONF_HOST]
- port = config_entry.data[CONF_PORT]
- token = config_entry.data.get(CONF_TOKEN)
+ host = entry.data[CONF_HOST]
+ port = entry.data[CONF_PORT]
+ token = entry.data.get(CONF_TOKEN)
hyperion_client = await async_create_connect_hyperion_client(
host, port, token=token, raw_connection=True
@@ -190,7 +191,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
# We need 1 root client (to manage instances being removed/added) and then 1 client
# per Hyperion server instance which is shared for all entities associated with
# that instance.
- hass.data[DOMAIN][config_entry.entry_id] = {
+ hass.data[DOMAIN][entry.entry_id] = {
CONF_ROOT_CLIENT: hyperion_client,
CONF_INSTANCE_CLIENTS: {},
CONF_ON_UNLOAD: [],
@@ -207,10 +208,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
device_registry = dr.async_get(hass)
running_instances: set[int] = set()
stopped_instances: set[int] = set()
- existing_instances = hass.data[DOMAIN][config_entry.entry_id][
- CONF_INSTANCE_CLIENTS
- ]
- server_id = cast(str, config_entry.unique_id)
+ existing_instances = hass.data[DOMAIN][entry.entry_id][CONF_INSTANCE_CLIENTS]
+ server_id = cast(str, entry.unique_id)
# In practice, an instance can be in 3 states as seen by this function:
#
@@ -239,7 +238,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
instance_name = instance.get(hyperion_const.KEY_FRIENDLY_NAME, DEFAULT_NAME)
async_dispatcher_send(
hass,
- SIGNAL_INSTANCE_ADD.format(config_entry.entry_id),
+ SIGNAL_INSTANCE_ADD.format(entry.entry_id),
instance_num,
instance_name,
)
@@ -248,7 +247,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
for instance_num in set(existing_instances) - running_instances:
del existing_instances[instance_num]
async_dispatcher_send(
- hass, SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id), instance_num
+ hass, SIGNAL_INSTANCE_REMOVE.format(entry.entry_id), instance_num
)
# Ensure every device associated with this config entry is still in the list of
@@ -258,7 +257,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
for instance_num in running_instances | stopped_instances
}
for device_entry in dr.async_entries_for_config_entry(
- device_registry, config_entry.entry_id
+ device_registry, entry.entry_id
):
for (kind, key) in device_entry.identifiers:
if kind == DOMAIN and key in known_devices:
@@ -275,15 +274,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async def setup_then_listen() -> None:
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
for platform in PLATFORMS
]
)
assert hyperion_client
if hyperion_client.instances is not None:
await async_instances_to_clients_raw(hyperion_client.instances)
- hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append(
- config_entry.add_update_listener(_async_entry_updated)
+ hass.data[DOMAIN][entry.entry_id][CONF_ON_UNLOAD].append(
+ entry.add_update_listener(_async_entry_updated)
)
hass.async_create_task(setup_then_listen())
diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py
new file mode 100644
index 00000000000..1ef88969228
--- /dev/null
+++ b/homeassistant/components/hyperion/camera.py
@@ -0,0 +1,264 @@
+"""Switch platform for Hyperion."""
+
+from __future__ import annotations
+
+import asyncio
+import base64
+import binascii
+from collections.abc import AsyncGenerator
+from contextlib import asynccontextmanager
+import functools
+import logging
+from typing import Any
+
+from aiohttp import web
+from hyperion import client
+from hyperion.const import (
+ KEY_IMAGE,
+ KEY_IMAGE_STREAM,
+ KEY_LEDCOLORS,
+ KEY_RESULT,
+ KEY_UPDATE,
+)
+
+from homeassistant.components.camera import (
+ DEFAULT_CONTENT_TYPE,
+ Camera,
+ async_get_still_stream,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import (
+ get_hyperion_device_id,
+ get_hyperion_unique_id,
+ listen_for_instance_updates,
+)
+from .const import (
+ CONF_INSTANCE_CLIENTS,
+ DOMAIN,
+ HYPERION_MANUFACTURER_NAME,
+ HYPERION_MODEL_NAME,
+ NAME_SUFFIX_HYPERION_CAMERA,
+ SIGNAL_ENTITY_REMOVE,
+ TYPE_HYPERION_CAMERA,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+IMAGE_STREAM_JPG_SENTINEL = "data:image/jpg;base64,"
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up a Hyperion platform from config entry."""
+ entry_data = hass.data[DOMAIN][config_entry.entry_id]
+ server_id = config_entry.unique_id
+
+ def camera_unique_id(instance_num: int) -> str:
+ """Return the camera unique_id."""
+ assert server_id
+ return get_hyperion_unique_id(server_id, instance_num, TYPE_HYPERION_CAMERA)
+
+ @callback
+ def instance_add(instance_num: int, instance_name: str) -> None:
+ """Add entities for a new Hyperion instance."""
+ assert server_id
+ async_add_entities(
+ [
+ HyperionCamera(
+ server_id,
+ instance_num,
+ instance_name,
+ entry_data[CONF_INSTANCE_CLIENTS][instance_num],
+ )
+ ]
+ )
+
+ @callback
+ def instance_remove(instance_num: int) -> None:
+ """Remove entities for an old Hyperion instance."""
+ assert server_id
+ async_dispatcher_send(
+ hass,
+ SIGNAL_ENTITY_REMOVE.format(
+ camera_unique_id(instance_num),
+ ),
+ )
+
+ listen_for_instance_updates(hass, config_entry, instance_add, instance_remove)
+
+
+# A note on Hyperion streaming semantics:
+#
+# Different Hyperion priorities behave different with regards to streaming. Colors will
+# not stream (as there is nothing to stream). External grabbers (e.g. USB Capture) will
+# stream what is being captured. Some effects (based on GIFs) will stream, others will
+# not. In cases when streaming is not supported from a selected priority, there is no
+# notification beyond the failure of new frames to arrive.
+
+
+class HyperionCamera(Camera):
+ """ComponentBinarySwitch switch class."""
+
+ def __init__(
+ self,
+ server_id: str,
+ instance_num: int,
+ instance_name: str,
+ hyperion_client: client.HyperionClient,
+ ) -> None:
+ """Initialize the switch."""
+ super().__init__()
+
+ self._unique_id = get_hyperion_unique_id(
+ server_id, instance_num, TYPE_HYPERION_CAMERA
+ )
+ self._name = f"{instance_name} {NAME_SUFFIX_HYPERION_CAMERA}".strip()
+ self._device_id = get_hyperion_device_id(server_id, instance_num)
+ self._instance_name = instance_name
+ self._client = hyperion_client
+
+ self._image_cond = asyncio.Condition()
+ self._image: bytes | None = None
+
+ # The number of open streams, when zero the stream is stopped.
+ self._image_stream_clients = 0
+
+ self._client_callbacks = {
+ f"{KEY_LEDCOLORS}-{KEY_IMAGE_STREAM}-{KEY_UPDATE}": self._update_imagestream
+ }
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique id for this instance."""
+ return self._unique_id
+
+ @property
+ def name(self) -> str:
+ """Return the name of the switch."""
+ return self._name
+
+ @property
+ def is_on(self) -> bool:
+ """Return true if the camera is on."""
+ return self.available
+
+ @property
+ def available(self) -> bool:
+ """Return server availability."""
+ return bool(self._client.has_loaded_state)
+
+ async def _update_imagestream(self, img: dict[str, Any] | None = None) -> None:
+ """Update Hyperion components."""
+ if not img:
+ return
+ img_data = img.get(KEY_RESULT, {}).get(KEY_IMAGE)
+ if not img_data or not img_data.startswith(IMAGE_STREAM_JPG_SENTINEL):
+ return
+ async with self._image_cond:
+ try:
+ self._image = base64.b64decode(
+ img_data[len(IMAGE_STREAM_JPG_SENTINEL) :]
+ )
+ except binascii.Error:
+ return
+ self._image_cond.notify_all()
+
+ async def _async_wait_for_camera_image(self) -> bytes | None:
+ """Return a single camera image in a stream."""
+ async with self._image_cond:
+ await self._image_cond.wait()
+ return self._image if self.available else None
+
+ async def _start_image_streaming_for_client(self) -> bool:
+ """Start streaming for a client."""
+ if (
+ not self._image_stream_clients
+ and not await self._client.async_send_image_stream_start()
+ ):
+ return False
+
+ self._image_stream_clients += 1
+ self.is_streaming = True
+ self.async_write_ha_state()
+ return True
+
+ async def _stop_image_streaming_for_client(self) -> None:
+ """Stop streaming for a client."""
+ self._image_stream_clients -= 1
+
+ if not self._image_stream_clients:
+ await self._client.async_send_image_stream_stop()
+ self.is_streaming = False
+ self.async_write_ha_state()
+
+ @asynccontextmanager
+ async def _image_streaming(self) -> AsyncGenerator:
+ """Async context manager to start/stop image streaming."""
+ try:
+ yield await self._start_image_streaming_for_client()
+ finally:
+ await self._stop_image_streaming_for_client()
+
+ async def async_camera_image(self) -> bytes | None:
+ """Return single camera image bytes."""
+ async with self._image_streaming() as is_streaming:
+ if is_streaming:
+ return await self._async_wait_for_camera_image()
+ return None
+
+ async def handle_async_mjpeg_stream(
+ self, request: web.Request
+ ) -> web.StreamResponse | None:
+ """Serve an HTTP MJPEG stream from the camera."""
+ async with self._image_streaming() as is_streaming:
+ if is_streaming:
+ return await async_get_still_stream(
+ request,
+ self._async_wait_for_camera_image,
+ DEFAULT_CONTENT_TYPE,
+ 0.0,
+ )
+ return None
+
+ async def async_added_to_hass(self) -> None:
+ """Register callbacks when entity added to hass."""
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ SIGNAL_ENTITY_REMOVE.format(self._unique_id),
+ functools.partial(self.async_remove, force_remove=True),
+ )
+ )
+
+ self._client.add_callbacks(self._client_callbacks)
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Cleanup prior to hass removal."""
+ self._client.remove_callbacks(self._client_callbacks)
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device information."""
+ return {
+ "identifiers": {(DOMAIN, self._device_id)},
+ "name": self._instance_name,
+ "manufacturer": HYPERION_MANUFACTURER_NAME,
+ "model": HYPERION_MODEL_NAME,
+ }
+
+
+CAMERA_TYPES = {
+ TYPE_HYPERION_CAMERA: HyperionCamera,
+}
diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py
index 9deeba9d019..271618fb962 100644
--- a/homeassistant/components/hyperion/const.py
+++ b/homeassistant/components/hyperion/const.py
@@ -24,11 +24,13 @@ HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9"
NAME_SUFFIX_HYPERION_LIGHT = ""
NAME_SUFFIX_HYPERION_PRIORITY_LIGHT = "Priority"
NAME_SUFFIX_HYPERION_COMPONENT_SWITCH = "Component"
+NAME_SUFFIX_HYPERION_CAMERA = ""
SIGNAL_INSTANCE_ADD = f"{DOMAIN}_instance_add_signal." "{}"
SIGNAL_INSTANCE_REMOVE = f"{DOMAIN}_instance_remove_signal." "{}"
SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal." "{}"
+TYPE_HYPERION_CAMERA = "hyperion_camera"
TYPE_HYPERION_LIGHT = "hyperion_light"
TYPE_HYPERION_PRIORITY_LIGHT = "hyperion_priority_light"
TYPE_HYPERION_COMPONENT_SWITCH_BASE = "hyperion_component_switch"
diff --git a/homeassistant/components/hyperion/translations/he.json b/homeassistant/components/hyperion/translations/he.json
new file mode 100644
index 00000000000..dd22953025f
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/he.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
+ "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\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_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "confirm": {
+ "description": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05e8\u05d5\u05e6\u05d4 \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea Hyperion Ambilight \u05d4\u05d6\u05d4 \u05dc-Home Assistant?\n\n**\u05de\u05d0\u05e8\u05d7:** {host}\n**\u05e4\u05ea\u05d7\u05d4:** {port}\n**\u05de\u05d6\u05d4\u05d4**: {id}"
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py
index a74eea7ba07..1fb35e23c9e 100644
--- a/homeassistant/components/ialarm/__init__.py
+++ b/homeassistant/components/ialarm/__init__.py
@@ -18,7 +18,7 @@ PLATFORMS = ["alarm_control_panel"]
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up iAlarm config."""
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
diff --git a/homeassistant/components/ialarm/translations/he.json b/homeassistant/components/ialarm/translations/he.json
new file mode 100644
index 00000000000..58521f503e2
--- /dev/null
+++ b/homeassistant/components/ialarm/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py
index 4ed9efd06f2..2bd0cade3b9 100644
--- a/homeassistant/components/iaqualink/__init__.py
+++ b/homeassistant/components/iaqualink/__init__.py
@@ -85,7 +85,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Aqualink from a config entry."""
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
diff --git a/homeassistant/components/iaqualink/translations/he.json b/homeassistant/components/iaqualink/translations/he.json
index 6f4191da70d..d704c432c35 100644
--- a/homeassistant/components/iaqualink/translations/he.json
+++ b/homeassistant/components/iaqualink/translations/he.json
@@ -1,10 +1,18 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea."
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
"step": {
"user": {
"data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
- }
+ },
+ "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 \u05d7\u05e9\u05d1\u05d5\u05df iAqualink \u05e9\u05dc\u05da."
}
}
}
diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py
index 0e1bda16d60..ec55a1fcedd 100644
--- a/homeassistant/components/icloud/sensor.py
+++ b/homeassistant/components/icloud/sensor.py
@@ -53,6 +53,9 @@ def add_entities(account, async_add_entities, tracked):
class IcloudDeviceBatterySensor(SensorEntity):
"""Representation of a iCloud device battery sensor."""
+ _attr_device_class = DEVICE_CLASS_BATTERY
+ _attr_unit_of_measurement = PERCENTAGE
+
def __init__(self, account: IcloudAccount, device: IcloudDevice) -> None:
"""Initialize the battery sensor."""
self._account = account
@@ -69,21 +72,11 @@ class IcloudDeviceBatterySensor(SensorEntity):
"""Sensor name."""
return f"{self._device.name} battery state"
- @property
- def device_class(self) -> str:
- """Return the device class of the sensor."""
- return DEVICE_CLASS_BATTERY
-
@property
def state(self) -> int:
"""Battery state percentage."""
return self._device.battery_level
- @property
- def unit_of_measurement(self) -> str:
- """Battery state measured in percentage."""
- return PERCENTAGE
-
@property
def icon(self) -> str:
"""Battery state icon handling."""
diff --git a/homeassistant/components/icloud/translations/he.json b/homeassistant/components/icloud/translations/he.json
index 139d7a1e399..73f09385a36 100644
--- a/homeassistant/components/icloud/translations/he.json
+++ b/homeassistant/components/icloud/translations/he.json
@@ -1,11 +1,42 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
"step": {
+ "reauth": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d4\u05d6\u05e0\u05ea \u05d1\u05e2\u05d1\u05e8 \u05e2\u05d1\u05d5\u05e8 {username} \u05d0\u05d9\u05e0\u05d4 \u05e4\u05d5\u05e2\u05dc\u05ea \u05e2\u05d5\u05d3. \u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d4\u05de\u05e9\u05d9\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e9\u05d9\u05dc\u05d5\u05d1 \u05d6\u05d4.",
+ "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1"
+ },
+ "trusted_device": {
+ "data": {
+ "trusted_device": "\u05de\u05db\u05e9\u05d9\u05e8 \u05de\u05d4\u05d9\u05de\u05df"
+ },
+ "description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05d4\u05de\u05d4\u05d9\u05de\u05df \u05e9\u05dc\u05da",
+ "title": "\u05de\u05db\u05e9\u05d9\u05e8 \u05de\u05d4\u05d9\u05de\u05df \u05e9\u05dc iCloud"
+ },
"user": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
- "username": "\u05d3\u05d5\u05d0\u05e8 \u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9"
- }
+ "username": "\u05d3\u05d5\u05d0\u05e8 \u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9",
+ "with_family": "\u05e2\u05dd \u05d4\u05de\u05e9\u05e4\u05d7\u05d4"
+ },
+ "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05d0\u05d9\u05e9\u05d5\u05e8\u05d9\u05dd \u05e9\u05dc\u05da",
+ "title": "\u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 iCloud"
+ },
+ "verification_code": {
+ "data": {
+ "verification_code": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea"
+ },
+ "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05d9\u05e0\u05d5 \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05e7\u05d9\u05d1\u05dc\u05ea\u05dd \u05d6\u05d4 \u05e2\u05ea\u05d4 \u05de-iCloud",
+ "title": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea iCloud"
}
}
}
diff --git a/homeassistant/components/ifttt/translations/he.json b/homeassistant/components/ifttt/translations/he.json
new file mode 100644
index 00000000000..ebee9aee976
--- /dev/null
+++ b/homeassistant/components/ifttt/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "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.",
+ "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py
index 314a7bdea31..2ef9cbf4eeb 100644
--- a/homeassistant/components/ign_sismologia/geo_location.py
+++ b/homeassistant/components/ign_sismologia/geo_location.py
@@ -132,6 +132,8 @@ class IgnSismologiaFeedEntityManager:
class IgnSismologiaLocationEvent(GeolocationEvent):
"""This represents an external event with IGN Sismologia feed data."""
+ _attr_unit_of_measurement = LENGTH_KILOMETERS
+
def __init__(self, feed_manager, external_id):
"""Initialize entity with data from feed entry."""
self._feed_manager = feed_manager
@@ -233,11 +235,6 @@ class IgnSismologiaLocationEvent(GeolocationEvent):
"""Return longitude value of this external event."""
return self._longitude
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return LENGTH_KILOMETERS
-
@property
def extra_state_attributes(self):
"""Return the device state attributes."""
diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json
index ce472e66449..e80e3a4eeec 100644
--- a/homeassistant/components/ign_sismologia/manifest.json
+++ b/homeassistant/components/ign_sismologia/manifest.json
@@ -2,7 +2,7 @@
"domain": "ign_sismologia",
"name": "IGN Sismolog\u00eda",
"documentation": "https://www.home-assistant.io/integrations/ign_sismologia",
- "requirements": ["georss_ign_sismologia_client==0.2"],
+ "requirements": ["georss_ign_sismologia_client==0.3"],
"codeowners": ["@exxamalte"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json
index 741fb8511a6..82b7e58a653 100644
--- a/homeassistant/components/image/manifest.json
+++ b/homeassistant/components/image/manifest.json
@@ -3,7 +3,7 @@
"name": "Image",
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/image",
- "requirements": ["pillow==8.1.2"],
+ "requirements": ["pillow==8.2.0"],
"dependencies": ["http"],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal"
diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json
index 5bb1efa0ca1..c1823459745 100644
--- a/homeassistant/components/imap/manifest.json
+++ b/homeassistant/components/imap/manifest.json
@@ -2,7 +2,7 @@
"domain": "imap",
"name": "IMAP",
"documentation": "https://www.home-assistant.io/integrations/imap",
- "requirements": ["aioimaplib==0.7.15"],
+ "requirements": ["aioimaplib==0.9.0"],
"codeowners": [],
"iot_class": "cloud_push"
}
diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py
index 661d58da989..f198d552781 100644
--- a/homeassistant/components/input_boolean/__init__.py
+++ b/homeassistant/components/input_boolean/__init__.py
@@ -2,6 +2,7 @@
from __future__ import annotations
import logging
+from typing import Any
import voluptuous as vol
@@ -77,7 +78,7 @@ class InputBooleanStorageCollection(collection.StorageCollection):
@bind_hass
-def is_on(hass, entity_id):
+def is_on(hass: HomeAssistant, entity_id: str) -> bool:
"""Test if input_boolean is True."""
return hass.states.is_state(entity_id, STATE_ON)
@@ -144,14 +145,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
class InputBoolean(ToggleEntity, RestoreEntity):
"""Representation of a boolean input."""
- def __init__(self, config: dict | None) -> None:
+ _attr_should_poll = False
+
+ def __init__(self, config: ConfigType) -> None:
"""Initialize a boolean input."""
self._config = config
self.editable = True
- self._state = config.get(CONF_INITIAL)
+ self._attr_is_on = config.get(CONF_INITIAL, False)
+ self._attr_unique_id = config[CONF_ID]
@classmethod
- def from_yaml(cls, config: dict) -> InputBoolean:
+ def from_yaml(cls, config: ConfigType) -> InputBoolean:
"""Return entity instance initialized from yaml storage."""
input_bool = cls(config)
input_bool.entity_id = f"{DOMAIN}.{config[CONF_ID]}"
@@ -159,56 +163,41 @@ class InputBoolean(ToggleEntity, RestoreEntity):
return input_bool
@property
- def should_poll(self):
- """If entity should be polled."""
- return False
-
- @property
- def name(self):
+ def name(self) -> str | None:
"""Return name of the boolean input."""
return self._config.get(CONF_NAME)
@property
- def extra_state_attributes(self):
- """Return the state attributes of the entity."""
- return {ATTR_EDITABLE: self.editable}
-
- @property
- def icon(self):
+ def icon(self) -> str | None:
"""Return the icon to be used for this entity."""
return self._config.get(CONF_ICON)
@property
- def is_on(self):
- """Return true if entity is on."""
- return self._state
+ def extra_state_attributes(self) -> dict[str, bool]:
+ """Return the state attributes of the entity."""
+ return {ATTR_EDITABLE: self.editable}
- @property
- def unique_id(self):
- """Return a unique ID for the person."""
- return self._config[CONF_ID]
-
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
- # If not None, we got an initial value.
+ # Don't restore if we got an initial value.
await super().async_added_to_hass()
- if self._state is not None:
+ if self._config.get(CONF_INITIAL) is not None:
return
state = await self.async_get_last_state()
- self._state = state and state.state == STATE_ON
+ self._attr_is_on = state is not None and state.state == STATE_ON
- async def async_turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
- self._state = True
+ self._attr_is_on = True
self.async_write_ha_state()
- async def async_turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
- self._state = False
+ self._attr_is_on = False
self.async_write_ha_state()
- async def async_update_config(self, config: dict) -> None:
+ async def async_update_config(self, config: ConfigType) -> None:
"""Handle when the config is updated."""
self._config = config
self.async_write_ha_state()
diff --git a/homeassistant/components/insteon/translations/he.json b/homeassistant/components/insteon/translations/he.json
index 8aabed0bfce..220bbcbb632 100644
--- a/homeassistant/components/insteon/translations/he.json
+++ b/homeassistant/components/insteon/translations/he.json
@@ -1,9 +1,47 @@
{
- "options": {
+ "config": {
+ "abort": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\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."
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
"step": {
+ "hubv1": {
+ "data": {
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ },
+ "hubv2": {
+ "data": {
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "plm": {
+ "data": {
+ "device": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "add_x10": {
+ "description": "\u05e9\u05e0\u05d4 \u05d0\u05ea \u05e1\u05d9\u05e1\u05de\u05ea \u05e8\u05db\u05d6\u05ea Insteon."
+ },
"change_hub_config": {
"data": {
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py
index 2feafba949d..6797da9d8a6 100644
--- a/homeassistant/components/ios/__init__.py
+++ b/homeassistant/components/ios/__init__.py
@@ -212,20 +212,20 @@ CONFIGURATION_FILE = ".ios.conf"
def devices_with_push(hass):
"""Return a dictionary of push enabled targets."""
- targets = {}
- for device_name, device in hass.data[DOMAIN][ATTR_DEVICES].items():
- if device.get(ATTR_PUSH_ID) is not None:
- targets[device_name] = device.get(ATTR_PUSH_ID)
- return targets
+ return {
+ device_name: device.get(ATTR_PUSH_ID)
+ for device_name, device in hass.data[DOMAIN][ATTR_DEVICES].items()
+ if device.get(ATTR_PUSH_ID) is not None
+ }
def enabled_push_ids(hass):
"""Return a list of push enabled target push IDs."""
- push_ids = []
- for device in hass.data[DOMAIN][ATTR_DEVICES].values():
- if device.get(ATTR_PUSH_ID) is not None:
- push_ids.append(device.get(ATTR_PUSH_ID))
- return push_ids
+ return [
+ device.get(ATTR_PUSH_ID)
+ for device in hass.data[DOMAIN][ATTR_DEVICES].values()
+ if device.get(ATTR_PUSH_ID) is not None
+ ]
def devices(hass):
@@ -337,14 +337,6 @@ class iOSIdentifyDeviceView(HomeAssistantView):
hass = request.app["hass"]
- # Commented for now while iOS app is getting frequent updates
- # try:
- # data = IDENTIFY_SCHEMA(req_data)
- # except vol.Invalid as ex:
- # return self.json_message(
- # vol.humanize.humanize_error(request.json, ex),
- # HTTP_BAD_REQUEST)
-
data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat()
device_id = data[ATTR_DEVICE_ID]
diff --git a/homeassistant/components/ios/translations/he.json b/homeassistant/components/ios/translations/he.json
index deb8eae6b38..a18f311e43a 100644
--- a/homeassistant/components/ios/translations/he.json
+++ b/homeassistant/components/ios/translations/he.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc Home Assistant iOS \u05e0\u05d7\u05d5\u05e6\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": {
"confirm": {
- "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Home Assistant iOS?"
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?"
}
}
}
diff --git a/homeassistant/components/ipma/translations/he.json b/homeassistant/components/ipma/translations/he.json
index c4818393edd..90ccdbdd48f 100644
--- a/homeassistant/components/ipma/translations/he.json
+++ b/homeassistant/components/ipma/translations/he.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
"longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
+ "mode": "\u05de\u05e6\u05d1",
"name": "\u05e9\u05dd"
},
"title": "\u05de\u05d9\u05e7\u05d5\u05dd"
diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py
index c1bc7ed4986..65d326f8f3a 100644
--- a/homeassistant/components/ipp/__init__.py
+++ b/homeassistant/components/ipp/__init__.py
@@ -1,40 +1,17 @@
"""The Internet Printing Protocol (IPP) integration."""
from __future__ import annotations
-from datetime import timedelta
import logging
-from pyipp import IPP, IPPError, Printer as IPPPrinter
-
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import (
- ATTR_NAME,
- CONF_HOST,
- CONF_PORT,
- CONF_SSL,
- CONF_VERIFY_SSL,
-)
+from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.entity import DeviceInfo
-from homeassistant.helpers.update_coordinator import (
- CoordinatorEntity,
- DataUpdateCoordinator,
- UpdateFailed,
-)
-from .const import (
- ATTR_IDENTIFIERS,
- ATTR_MANUFACTURER,
- ATTR_MODEL,
- ATTR_SOFTWARE_VERSION,
- CONF_BASE_PATH,
- DOMAIN,
-)
+from .const import CONF_BASE_PATH, DOMAIN
+from .coordinator import IPPDataUpdateCoordinator
PLATFORMS = [SENSOR_DOMAIN]
-SCAN_INTERVAL = timedelta(seconds=60)
_LOGGER = logging.getLogger(__name__)
@@ -68,92 +45,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
-
-
-class IPPDataUpdateCoordinator(DataUpdateCoordinator[IPPPrinter]):
- """Class to manage fetching IPP data from single endpoint."""
-
- def __init__(
- self,
- hass: HomeAssistant,
- *,
- host: str,
- port: int,
- base_path: str,
- tls: bool,
- verify_ssl: bool,
- ) -> None:
- """Initialize global IPP data updater."""
- self.ipp = IPP(
- host=host,
- port=port,
- base_path=base_path,
- tls=tls,
- verify_ssl=verify_ssl,
- session=async_get_clientsession(hass, verify_ssl),
- )
-
- super().__init__(
- hass,
- _LOGGER,
- name=DOMAIN,
- update_interval=SCAN_INTERVAL,
- )
-
- async def _async_update_data(self) -> IPPPrinter:
- """Fetch data from IPP."""
- try:
- return await self.ipp.printer()
- except IPPError as error:
- raise UpdateFailed(f"Invalid response from API: {error}") from error
-
-
-class IPPEntity(CoordinatorEntity):
- """Defines a base IPP entity."""
-
- def __init__(
- self,
- *,
- entry_id: str,
- device_id: str,
- coordinator: IPPDataUpdateCoordinator,
- name: str,
- icon: str,
- enabled_default: bool = True,
- ) -> None:
- """Initialize the IPP entity."""
- super().__init__(coordinator)
- self._device_id = device_id
- self._enabled_default = enabled_default
- self._entry_id = entry_id
- self._icon = icon
- self._name = name
-
- @property
- def name(self) -> str:
- """Return the name of the entity."""
- return self._name
-
- @property
- def icon(self) -> str:
- """Return the mdi icon of the entity."""
- return self._icon
-
- @property
- def entity_registry_enabled_default(self) -> bool:
- """Return if the entity should be enabled when first added to the entity registry."""
- return self._enabled_default
-
- @property
- def device_info(self) -> DeviceInfo:
- """Return device information about this IPP device."""
- if self._device_id is None:
- return None
-
- return {
- ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)},
- ATTR_NAME: self.coordinator.data.info.name,
- ATTR_MANUFACTURER: self.coordinator.data.info.manufacturer,
- ATTR_MODEL: self.coordinator.data.info.model,
- ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version,
- }
diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py
new file mode 100644
index 00000000000..abc97dd3dd2
--- /dev/null
+++ b/homeassistant/components/ipp/coordinator.py
@@ -0,0 +1,55 @@
+"""Coordinator for The Internet Printing Protocol (IPP) integration."""
+from __future__ import annotations
+
+from datetime import timedelta
+import logging
+
+from pyipp import IPP, IPPError, Printer as IPPPrinter
+
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import DOMAIN
+
+SCAN_INTERVAL = timedelta(seconds=60)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class IPPDataUpdateCoordinator(DataUpdateCoordinator[IPPPrinter]):
+ """Class to manage fetching IPP data from single endpoint."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ *,
+ host: str,
+ port: int,
+ base_path: str,
+ tls: bool,
+ verify_ssl: bool,
+ ) -> None:
+ """Initialize global IPP data updater."""
+ self.ipp = IPP(
+ host=host,
+ port=port,
+ base_path=base_path,
+ tls=tls,
+ verify_ssl=verify_ssl,
+ session=async_get_clientsession(hass, verify_ssl),
+ )
+
+ super().__init__(
+ hass,
+ _LOGGER,
+ name=DOMAIN,
+ update_interval=SCAN_INTERVAL,
+ )
+
+ async def _async_update_data(self) -> IPPPrinter:
+ """Fetch data from IPP."""
+ try:
+ return await self.ipp.printer()
+ except IPPError as error:
+ raise UpdateFailed(f"Invalid response from API: {error}") from error
diff --git a/homeassistant/components/ipp/entity.py b/homeassistant/components/ipp/entity.py
new file mode 100644
index 00000000000..0038bbd7370
--- /dev/null
+++ b/homeassistant/components/ipp/entity.py
@@ -0,0 +1,51 @@
+"""Entities for The Internet Printing Protocol (IPP) integration."""
+from __future__ import annotations
+
+from homeassistant.const import ATTR_NAME
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import (
+ ATTR_IDENTIFIERS,
+ ATTR_MANUFACTURER,
+ ATTR_MODEL,
+ ATTR_SOFTWARE_VERSION,
+ DOMAIN,
+)
+from .coordinator import IPPDataUpdateCoordinator
+
+
+class IPPEntity(CoordinatorEntity):
+ """Defines a base IPP entity."""
+
+ def __init__(
+ self,
+ *,
+ entry_id: str,
+ device_id: str,
+ coordinator: IPPDataUpdateCoordinator,
+ name: str,
+ icon: str,
+ enabled_default: bool = True,
+ ) -> None:
+ """Initialize the IPP entity."""
+ super().__init__(coordinator)
+ self._device_id = device_id
+ self._entry_id = entry_id
+ self._attr_name = name
+ self._attr_icon = icon
+ self._attr_entity_registry_enabled_default = enabled_default
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device information about this IPP device."""
+ if self._device_id is None:
+ return None
+
+ return {
+ ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)},
+ ATTR_NAME: self.coordinator.data.info.name,
+ ATTR_MANUFACTURER: self.coordinator.data.info.manufacturer,
+ ATTR_MODEL: self.coordinator.data.info.model,
+ ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version,
+ }
diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py
index 0d6dbdff065..5d736c864e1 100644
--- a/homeassistant/components/ipp/sensor.py
+++ b/homeassistant/components/ipp/sensor.py
@@ -11,7 +11,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utcnow
-from . import IPPDataUpdateCoordinator, IPPEntity
from .const import (
ATTR_COMMAND_SET,
ATTR_INFO,
@@ -24,6 +23,8 @@ from .const import (
ATTR_URI_SUPPORTED,
DOMAIN,
)
+from .coordinator import IPPDataUpdateCoordinator
+from .entity import IPPEntity
async def async_setup_entry(
@@ -69,12 +70,9 @@ class IPPSensor(IPPEntity, SensorEntity):
unit_of_measurement: str | None = None,
) -> None:
"""Initialize IPP sensor."""
- self._unit_of_measurement = unit_of_measurement
self._key = key
- self._unique_id = None
-
- if unique_id is not None:
- self._unique_id = f"{unique_id}_{key}"
+ self._attr_unique_id = f"{unique_id}_{key}"
+ self._attr_unit_of_measurement = unit_of_measurement
super().__init__(
entry_id=entry_id,
@@ -85,16 +83,6 @@ class IPPSensor(IPPEntity, SensorEntity):
enabled_default=enabled_default,
)
- @property
- def unique_id(self) -> str:
- """Return the unique ID for this sensor."""
- return self._unique_id
-
- @property
- def unit_of_measurement(self) -> str:
- """Return the unit this state is expressed in."""
- return self._unit_of_measurement
-
class IPPMarkerSensor(IPPSensor):
"""Defines an IPP marker sensor."""
@@ -184,6 +172,8 @@ class IPPPrinterSensor(IPPSensor):
class IPPUptimeSensor(IPPSensor):
"""Defines a IPP uptime sensor."""
+ _attr_device_class = DEVICE_CLASS_TIMESTAMP
+
def __init__(
self, entry_id: str, unique_id: str, coordinator: IPPDataUpdateCoordinator
) -> None:
@@ -203,8 +193,3 @@ class IPPUptimeSensor(IPPSensor):
"""Return the state of the sensor."""
uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime)
return uptime.replace(microsecond=0).isoformat()
-
- @property
- def device_class(self) -> str | None:
- """Return the class of this sensor."""
- return DEVICE_CLASS_TIMESTAMP
diff --git a/homeassistant/components/ipp/translations/de.json b/homeassistant/components/ipp/translations/de.json
index 69402c8fdba..80497c0c874 100644
--- a/homeassistant/components/ipp/translations/de.json
+++ b/homeassistant/components/ipp/translations/de.json
@@ -13,7 +13,7 @@
"cannot_connect": "Verbindung fehlgeschlagen",
"connection_upgrade": "Verbindung zum Drucker fehlgeschlagen. Bitte versuchen Sie es erneut mit aktivierter SSL / TLS-Option."
},
- "flow_title": "Drucker: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/ipp/translations/he.json b/homeassistant/components/ipp/translations/he.json
new file mode 100644
index 00000000000..6c93f705a48
--- /dev/null
+++ b/homeassistant/components/ipp/translations/he.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json
index feba8470d7e..b44904095de 100644
--- a/homeassistant/components/ipp/translations/pl.json
+++ b/homeassistant/components/ipp/translations/pl.json
@@ -23,7 +23,7 @@
"ssl": "Certyfikat SSL",
"verify_ssl": "Weryfikacja certyfikatu SSL"
},
- "description": "Skonfiguruj drukark\u0119 za pomoc\u0105 protoko\u0142u IPP (Internet Printing Protocol) w celu integracji z Home Assistantem.",
+ "description": "Skonfiguruj drukark\u0119 za pomoc\u0105 protoko\u0142u IPP (Internet Printing Protocol), aby zintegrowa\u0107 j\u0105 z Home Assistantem.",
"title": "Po\u0142\u0105cz swoj\u0105 drukark\u0119"
},
"zeroconf_confirm": {
diff --git a/homeassistant/components/iqvia/translations/he.json b/homeassistant/components/iqvia/translations/he.json
new file mode 100644
index 00000000000..48a6eeeea33
--- /dev/null
+++ b/homeassistant/components/iqvia/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/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json
index af6d09d0302..e72eb0a6da7 100644
--- a/homeassistant/components/islamic_prayer_times/manifest.json
+++ b/homeassistant/components/islamic_prayer_times/manifest.json
@@ -2,7 +2,7 @@
"domain": "islamic_prayer_times",
"name": "Islamic Prayer Times",
"documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times",
- "requirements": ["prayer_times_calculator==0.0.3"],
+ "requirements": ["prayer_times_calculator==0.0.5"],
"codeowners": ["@engrbm87"],
"config_flow": true,
"iot_class": "cloud_polling"
diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py
index 3133320d978..2fa563785d2 100644
--- a/homeassistant/components/islamic_prayer_times/sensor.py
+++ b/homeassistant/components/islamic_prayer_times/sensor.py
@@ -23,6 +23,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class IslamicPrayerTimeSensor(SensorEntity):
"""Representation of an Islamic prayer time sensor."""
+ _attr_device_class = DEVICE_CLASS_TIMESTAMP
+ _attr_icon = PRAYER_TIMES_ICON
+ _attr_should_poll = False
+
def __init__(self, sensor_type, client):
"""Initialize the Islamic prayer time sensor."""
self.sensor_type = sensor_type
@@ -38,11 +42,6 @@ class IslamicPrayerTimeSensor(SensorEntity):
"""Return the unique id of the entity."""
return self.sensor_type
- @property
- def icon(self):
- """Icon to display in the front end."""
- return PRAYER_TIMES_ICON
-
@property
def state(self):
"""Return the state of the sensor."""
@@ -52,16 +51,6 @@ class IslamicPrayerTimeSensor(SensorEntity):
.isoformat()
)
- @property
- def should_poll(self):
- """Disable polling."""
- return False
-
- @property
- def device_class(self):
- """Return the device class."""
- return DEVICE_CLASS_TIMESTAMP
-
async def async_added_to_hass(self):
"""Handle entity which will be added."""
self.async_on_remove(
diff --git a/homeassistant/components/islamic_prayer_times/translations/he.json b/homeassistant/components/islamic_prayer_times/translations/he.json
new file mode 100644
index 00000000000..d0c3523da94
--- /dev/null
+++ b/homeassistant/components/islamic_prayer_times/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/isy994/entity.py b/homeassistant/components/isy994/entity.py
index 69714dd9f4b..0406fc45cba 100644
--- a/homeassistant/components/isy994/entity.py
+++ b/homeassistant/components/isy994/entity.py
@@ -180,7 +180,7 @@ class ISYNodeEntity(ISYEntity):
await self._node.send_cmd(command, value, unit_of_measurement, parameters)
async def async_get_zwave_parameter(self, parameter):
- """Repsond to an entity service command to request a Z-Wave device parameter from the ISY."""
+ """Respond to an entity service command to request a Z-Wave device parameter from the ISY."""
if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE:
raise HomeAssistantError(
f"Invalid service call: cannot request Z-Wave Parameter for non-Z-Wave device {self.entity_id}"
@@ -188,7 +188,7 @@ class ISYNodeEntity(ISYEntity):
await self._node.get_zwave_parameter(parameter)
async def async_set_zwave_parameter(self, parameter, value, size):
- """Repsond to an entity service command to set a Z-Wave device parameter via the ISY."""
+ """Respond to an entity service command to set a Z-Wave device parameter via the ISY."""
if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE:
raise HomeAssistantError(
f"Invalid service call: cannot set Z-Wave Parameter for non-Z-Wave device {self.entity_id}"
@@ -197,7 +197,7 @@ class ISYNodeEntity(ISYEntity):
await self._node.get_zwave_parameter(parameter)
async def async_rename_node(self, name):
- """Repsond to an entity service command to rename a node on the ISY."""
+ """Respond to an entity service command to rename a node on the ISY."""
await self._node.rename(name)
diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py
index 73b5bd683ba..f9f3c6e5459 100644
--- a/homeassistant/components/isy994/fan.py
+++ b/homeassistant/components/isy994/fan.py
@@ -83,7 +83,7 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
**kwargs,
) -> None:
"""Send the turn on command to the ISY994 fan device."""
- await self.async_set_percentage(percentage)
+ await self.async_set_percentage(percentage or 67)
async def async_turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 fan device."""
diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json
index 00397ca5dd8..a6e9e0a1498 100644
--- a/homeassistant/components/isy994/translations/de.json
+++ b/homeassistant/components/isy994/translations/de.json
@@ -9,7 +9,7 @@
"invalid_host": "Der Hosteintrag hatte nicht das vollst\u00e4ndige URL-Format, z. B. http://192.168.10.100:80",
"unknown": "Unerwarteter Fehler"
},
- "flow_title": "Universalger\u00e4te ISY994 {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
@@ -39,7 +39,10 @@
},
"system_health": {
"info": {
- "host_reachable": "Deutsch"
+ "device_connected": "ISY verbunden",
+ "host_reachable": "Deutsch",
+ "last_heartbeat": "Letzte Heartbeat-Zeit",
+ "websocket_status": "Ereignis-Socket Status"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json
index 06824938c9d..46dc3260f83 100644
--- a/homeassistant/components/isy994/translations/es.json
+++ b/homeassistant/components/isy994/translations/es.json
@@ -36,5 +36,13 @@
"title": "Opciones ISY994"
}
}
+ },
+ "system_health": {
+ "info": {
+ "device_connected": "ISY conectado",
+ "host_reachable": "Anfitri\u00f3n accesible",
+ "last_heartbeat": "Hora del \u00faltimo latido",
+ "websocket_status": "Estado del conector de eventos"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/isy994/translations/he.json b/homeassistant/components/isy994/translations/he.json
index ed6f54fc696..2485285743f 100644
--- a/homeassistant/components/isy994/translations/he.json
+++ b/homeassistant/components/isy994/translations/he.json
@@ -5,17 +5,25 @@
},
"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",
"invalid_host": "\u05e2\u05e8\u05da \u05d4\u05beHost \u05dc\u05d0 \u05d4\u05d9\u05d4 \u05d1\u05e4\u05d5\u05e8\u05de\u05d8 URL \u05de\u05dc\u05d0, \u05dc\u05de\u05e9\u05dc, http://192.168.10.100:80",
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05e6\u05e4\u05d5\u05d9\u05d9\u05d4"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
"host": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
- }
+ },
+ "description": "\u05d4\u05e2\u05e8\u05da \u05d4\u05de\u05d0\u05e8\u05d7 \u05d7\u05d9\u05d9\u05d1 \u05dc\u05d4\u05d9\u05d5\u05ea \u05d1\u05e4\u05d5\u05e8\u05de\u05d8 URL \u05de\u05dc\u05d0, \u05dc\u05de\u05e9\u05dc, http://192.168.10.100:80"
}
}
+ },
+ "system_health": {
+ "info": {
+ "host_reachable": "\u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05d2\u05d9\u05e2 \u05dc\u05de\u05d0\u05e8\u05d7"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json
index 427a51157bf..ca8646ec584 100644
--- a/homeassistant/components/isy994/translations/hu.json
+++ b/homeassistant/components/isy994/translations/hu.json
@@ -8,7 +8,7 @@
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "Universal Devices ISY994 {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/isy994/translations/pl.json b/homeassistant/components/isy994/translations/pl.json
index c0ee1b5d3a3..4deeefe391d 100644
--- a/homeassistant/components/isy994/translations/pl.json
+++ b/homeassistant/components/isy994/translations/pl.json
@@ -36,5 +36,13 @@
"title": "Opcje ISY994"
}
}
+ },
+ "system_health": {
+ "info": {
+ "device_connected": "ISY pod\u0142\u0105czony",
+ "host_reachable": "Host osi\u0105galny",
+ "last_heartbeat": "Czas ostatniego pulsu",
+ "websocket_status": "Status wydarzenia websocket"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py
index 253bdc6cb2b..968f13748b2 100644
--- a/homeassistant/components/izone/climate.py
+++ b/homeassistant/components/izone/climate.py
@@ -12,6 +12,7 @@ from homeassistant.components.climate.const import (
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
+ FAN_TOP,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
HVAC_MODE_FAN_ONLY,
@@ -54,6 +55,7 @@ _IZONE_FAN_TO_HA = {
Controller.Fan.LOW: FAN_LOW,
Controller.Fan.MED: FAN_MEDIUM,
Controller.Fan.HIGH: FAN_HIGH,
+ Controller.Fan.TOP: FAN_TOP,
Controller.Fan.AUTO: FAN_AUTO,
}
diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json
index 0a2b8f82fe5..6da770f5c0b 100644
--- a/homeassistant/components/izone/manifest.json
+++ b/homeassistant/components/izone/manifest.json
@@ -2,7 +2,7 @@
"domain": "izone",
"name": "iZone",
"documentation": "https://www.home-assistant.io/integrations/izone",
- "requirements": ["python-izone==1.1.4"],
+ "requirements": ["python-izone==1.1.6"],
"codeowners": ["@Swamp-Ig"],
"config_flow": true,
"homekit": {
diff --git a/homeassistant/components/izone/translations/he.json b/homeassistant/components/izone/translations/he.json
new file mode 100644
index 00000000000..380dbc5d7fc
--- /dev/null
+++ b/homeassistant/components/izone/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py
index 28789849944..38089a6e17f 100644
--- a/homeassistant/components/juicenet/__init__.py
+++ b/homeassistant/components/juicenet/__init__.py
@@ -46,7 +46,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up JuiceNet from a config entry."""
config = entry.data
diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py
index d908dc069ef..7564c6e4344 100644
--- a/homeassistant/components/juicenet/sensor.py
+++ b/homeassistant/components/juicenet/sensor.py
@@ -1,5 +1,5 @@
"""Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors."""
-from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
from homeassistant.const import (
ELECTRICAL_CURRENT_AMPERE,
ENERGY_WATT_HOUR,
@@ -13,13 +13,13 @@ from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .entity import JuiceNetDevice
SENSOR_TYPES = {
- "status": ["Charging Status", None],
- "temperature": ["Temperature", TEMP_CELSIUS],
- "voltage": ["Voltage", VOLT],
- "amps": ["Amps", ELECTRICAL_CURRENT_AMPERE],
- "watts": ["Watts", POWER_WATT],
- "charge_time": ["Charge time", TIME_SECONDS],
- "energy_added": ["Energy added", ENERGY_WATT_HOUR],
+ "status": ["Charging Status", None, None],
+ "temperature": ["Temperature", TEMP_CELSIUS, STATE_CLASS_MEASUREMENT],
+ "voltage": ["Voltage", VOLT, None],
+ "amps": ["Amps", ELECTRICAL_CURRENT_AMPERE, STATE_CLASS_MEASUREMENT],
+ "watts": ["Watts", POWER_WATT, STATE_CLASS_MEASUREMENT],
+ "charge_time": ["Charge time", TIME_SECONDS, None],
+ "energy_added": ["Energy added", ENERGY_WATT_HOUR, None],
}
@@ -44,6 +44,7 @@ class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity):
super().__init__(device, sensor_type, coordinator)
self._name = SENSOR_TYPES[sensor_type][0]
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
+ self._attr_state_class = SENSOR_TYPES[sensor_type][2]
@property
def name(self):
diff --git a/homeassistant/components/juicenet/translations/he.json b/homeassistant/components/juicenet/translations/he.json
index 863e2560cbd..384ea203a51 100644
--- a/homeassistant/components/juicenet/translations/he.json
+++ b/homeassistant/components/juicenet/translations/he.json
@@ -1,11 +1,18 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
"error": {
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4"
},
"step": {
"user": {
+ "data": {
+ "api_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API"
+ },
"description": "\u05ea\u05d6\u05d3\u05e7\u05e7 \u05dc\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05beAPI \u05de\u05behttps://home.juice.net/Manage."
}
}
diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py
index 473acac57cd..2af7434d2e4 100644
--- a/homeassistant/components/keenetic_ndms2/__init__.py
+++ b/homeassistant/components/keenetic_ndms2/__init__.py
@@ -29,22 +29,22 @@ PLATFORMS = [BINARY_SENSOR_DOMAIN, DEVICE_TRACKER_DOMAIN]
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the component."""
hass.data.setdefault(DOMAIN, {})
- async_add_defaults(hass, config_entry)
+ async_add_defaults(hass, entry)
- router = KeeneticRouter(hass, config_entry)
+ router = KeeneticRouter(hass, entry)
await router.async_setup()
- undo_listener = config_entry.add_update_listener(update_listener)
+ undo_listener = entry.add_update_listener(update_listener)
- hass.data[DOMAIN][config_entry.entry_id] = {
+ hass.data[DOMAIN][entry.entry_id] = {
ROUTER: router,
UNDO_UPDATE_LISTENER: undo_listener,
}
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
@@ -97,14 +97,14 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
return unload_ok
-async def update_listener(hass, config_entry):
+async def update_listener(hass, entry):
"""Handle options update."""
- await hass.config_entries.async_reload(config_entry.entry_id)
+ await hass.config_entries.async_reload(entry.entry_id)
-def async_add_defaults(hass: HomeAssistant, config_entry: ConfigEntry):
+def async_add_defaults(hass: HomeAssistant, entry: ConfigEntry):
"""Populate default options."""
- host: str = config_entry.data[CONF_HOST]
+ host: str = entry.data[CONF_HOST]
imported_options: dict = hass.data[DOMAIN].get(f"imported_options_{host}", {})
options = {
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
@@ -114,8 +114,8 @@ def async_add_defaults(hass: HomeAssistant, config_entry: ConfigEntry):
CONF_INCLUDE_ARP: True,
CONF_INCLUDE_ASSOCIATED: True,
**imported_options,
- **config_entry.options,
+ **entry.options,
}
- if options.keys() - config_entry.options.keys():
- hass.config_entries.async_update_entry(config_entry, options=options)
+ if options.keys() - entry.options.keys():
+ hass.config_entries.async_update_entry(entry, options=options)
diff --git a/homeassistant/components/keenetic_ndms2/binary_sensor.py b/homeassistant/components/keenetic_ndms2/binary_sensor.py
index ed366bd7402..e8f7df02489 100644
--- a/homeassistant/components/keenetic_ndms2/binary_sensor.py
+++ b/homeassistant/components/keenetic_ndms2/binary_sensor.py
@@ -27,6 +27,9 @@ async def async_setup_entry(
class RouterOnlineBinarySensor(BinarySensorEntity):
"""Representation router connection status."""
+ _attr_device_class = DEVICE_CLASS_CONNECTIVITY
+ _attr_should_poll = False
+
def __init__(self, router: KeeneticRouter) -> None:
"""Initialize the APCUPSd binary device."""
self._router = router
@@ -46,16 +49,6 @@ class RouterOnlineBinarySensor(BinarySensorEntity):
"""Return true if the UPS is online, else false."""
return self._router.available
- @property
- def device_class(self):
- """Return the class of this device, from component DEVICE_CLASSES."""
- return DEVICE_CLASS_CONNECTIVITY
-
- @property
- def should_poll(self) -> bool:
- """Return False since entity pushes its state to HA."""
- return False
-
@property
def device_info(self):
"""Return a client description for device registry."""
diff --git a/homeassistant/components/keenetic_ndms2/translations/ca.json b/homeassistant/components/keenetic_ndms2/translations/ca.json
index 364b5dad10e..0acb0ef0266 100644
--- a/homeassistant/components/keenetic_ndms2/translations/ca.json
+++ b/homeassistant/components/keenetic_ndms2/translations/ca.json
@@ -1,11 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "El compte ja ha estat configurat"
+ "already_configured": "El compte ja ha estat configurat",
+ "no_udn": "La informaci\u00f3 de descobriment SSDP no t\u00e9 UDN",
+ "not_keenetic_ndms2": "El dispositiu descobert no \u00e9s un router Keenetic"
},
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/keenetic_ndms2/translations/de.json b/homeassistant/components/keenetic_ndms2/translations/de.json
index 68de4001255..c165cbd9043 100644
--- a/homeassistant/components/keenetic_ndms2/translations/de.json
+++ b/homeassistant/components/keenetic_ndms2/translations/de.json
@@ -1,11 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "Konto wurde bereits konfiguriert"
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "no_udn": "SSDP-Erkennungsinfo hat keine UDN",
+ "not_keenetic_ndms2": "Gefundenes Ger\u00e4t ist kein Keenetic-Router"
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
@@ -13,7 +16,8 @@
"password": "Passwort",
"port": "Port",
"username": "Benutzername"
- }
+ },
+ "title": "Keenetic NDMS2 Router einrichten"
}
}
},
@@ -21,8 +25,12 @@
"step": {
"user": {
"data": {
+ "consider_home": "Heimintervall ber\u00fccksichtigen",
+ "include_arp": "ARP-Daten verwenden (wird ignoriert, wenn Hotspot-Daten verwendet werden)",
+ "include_associated": "WLAN-AP-Zuordnungsdaten verwenden (wird ignoriert, wenn Hotspot-Daten verwendet werden)",
"interfaces": "Schnittstellen zum Scannen ausw\u00e4hlen",
- "scan_interval": "Scanintervall"
+ "scan_interval": "Scanintervall",
+ "try_hotspot": "'ip hotspot'-Daten verwenden (am genauesten)"
}
}
}
diff --git a/homeassistant/components/keenetic_ndms2/translations/es.json b/homeassistant/components/keenetic_ndms2/translations/es.json
index 6b8eed6e26b..190caf9947b 100644
--- a/homeassistant/components/keenetic_ndms2/translations/es.json
+++ b/homeassistant/components/keenetic_ndms2/translations/es.json
@@ -1,11 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "La cuenta ya est\u00e1 configurada"
+ "already_configured": "La cuenta ya est\u00e1 configurada",
+ "no_udn": "La informaci\u00f3n de descubrimiento SSDP no tiene UDN",
+ "not_keenetic_ndms2": "El art\u00edculo descubierto no es un router Keenetic"
},
"error": {
"cannot_connect": "Fallo de conexi\u00f3n"
},
+ "flow_title": "{name} ( {host} )",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/keenetic_ndms2/translations/he.json b/homeassistant/components/keenetic_ndms2/translations/he.json
new file mode 100644
index 00000000000..fd25964fcd0
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/hu.json b/homeassistant/components/keenetic_ndms2/translations/hu.json
index b545f065ddc..c1d27e9ae07 100644
--- a/homeassistant/components/keenetic_ndms2/translations/hu.json
+++ b/homeassistant/components/keenetic_ndms2/translations/hu.json
@@ -6,6 +6,7 @@
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/keenetic_ndms2/translations/it.json b/homeassistant/components/keenetic_ndms2/translations/it.json
index e19961f5823..8ce00bbdd81 100644
--- a/homeassistant/components/keenetic_ndms2/translations/it.json
+++ b/homeassistant/components/keenetic_ndms2/translations/it.json
@@ -1,11 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "L'account \u00e8 gi\u00e0 configurato"
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
+ "no_udn": "Le informazioni di rilevamento SSDP non hanno UDN",
+ "not_keenetic_ndms2": "L'elemento rilevato non \u00e8 un router Keenetic"
},
"error": {
"cannot_connect": "Impossibile connettersi"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/keenetic_ndms2/translations/nl.json b/homeassistant/components/keenetic_ndms2/translations/nl.json
index d2a85c8b059..3dd5b56c51d 100644
--- a/homeassistant/components/keenetic_ndms2/translations/nl.json
+++ b/homeassistant/components/keenetic_ndms2/translations/nl.json
@@ -1,11 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "Account is al geconfigureerd"
+ "already_configured": "Account is al geconfigureerd",
+ "no_udn": "SSDP-ontdekkingsinformatie heeft geen UDN",
+ "not_keenetic_ndms2": "Ontdekt item is geen Keenetic router"
},
"error": {
"cannot_connect": "Kan geen verbinding maken"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/keenetic_ndms2/translations/no.json b/homeassistant/components/keenetic_ndms2/translations/no.json
index d37a306d9a6..52200ccc615 100644
--- a/homeassistant/components/keenetic_ndms2/translations/no.json
+++ b/homeassistant/components/keenetic_ndms2/translations/no.json
@@ -1,11 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "Kontoen er allerede konfigurert"
+ "already_configured": "Kontoen er allerede konfigurert",
+ "no_udn": "SSDP-oppdagelsesinformasjon har ingen UDN",
+ "not_keenetic_ndms2": "Oppdaget element er ikke en Keenetic-router"
},
"error": {
"cannot_connect": "Tilkobling mislyktes"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/keenetic_ndms2/translations/pl.json b/homeassistant/components/keenetic_ndms2/translations/pl.json
index ba8b709301d..a96f50ecc19 100644
--- a/homeassistant/components/keenetic_ndms2/translations/pl.json
+++ b/homeassistant/components/keenetic_ndms2/translations/pl.json
@@ -1,11 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane"
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "no_udn": "Informacje o wykrywaniu SSDP nie maj\u0105 UDN",
+ "not_keenetic_ndms2": "Wykryty element nie jest routerem Keenetic"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json b/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json
index 2f64eee7439..616ccff6c20 100644
--- a/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json
+++ b/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json
@@ -1,11 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "no_udn": "SSDP \u6240\u767c\u73fe\u7684\u8cc7\u8a0a\u4e0d\u542b UDN",
+ "not_keenetic_ndms2": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Keenetic \u8def\u7531\u5668"
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py
index 9452e24a4f2..f32f825acc4 100644
--- a/homeassistant/components/kef/media_player.py
+++ b/homeassistant/components/kef/media_player.py
@@ -256,7 +256,7 @@ class KefMediaPlayer(MediaPlayerEntity):
self._source = None
self._volume = None
self._state = STATE_OFF
- except (ConnectionRefusedError, ConnectionError, TimeoutError) as err:
+ except (ConnectionError, TimeoutError) as err:
_LOGGER.debug("Error in `update`: %s", err)
self._state = None
diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py
index 2ada56e1c44..1d16dd12cc2 100644
--- a/homeassistant/components/keyboard_remote/__init__.py
+++ b/homeassistant/components/keyboard_remote/__init__.py
@@ -161,7 +161,7 @@ class KeyboardRemote:
# devices are often added and then correct permissions set after
try:
dev = InputDevice(descriptor)
- except (OSError, PermissionError):
+ except OSError:
return (None, None)
handler = None
@@ -318,7 +318,7 @@ class KeyboardRemote:
):
repeat_tasks[event.code].cancel()
del repeat_tasks[event.code]
- except (OSError, PermissionError, asyncio.CancelledError):
+ except (OSError, asyncio.CancelledError):
# cancel key repeat tasks
for task in repeat_tasks.values():
task.cancel()
diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py
index 7dd5b087a87..5226dfb4f26 100644
--- a/homeassistant/components/kmtronic/__init__.py
+++ b/homeassistant/components/kmtronic/__init__.py
@@ -20,7 +20,7 @@ PLATFORMS = ["switch"]
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up kmtronic from a config entry."""
session = aiohttp_client.async_get_clientsession(hass)
auth = Auth(
diff --git a/homeassistant/components/kmtronic/translations/he.json b/homeassistant/components/kmtronic/translations/he.json
new file mode 100644
index 00000000000..479d2f2f5e8
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "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",
+ "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": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py
index e98e598af1d..b56331bc80c 100644
--- a/homeassistant/components/knx/__init__.py
+++ b/homeassistant/components/knx/__init__.py
@@ -3,18 +3,14 @@ from __future__ import annotations
import asyncio
import logging
+from typing import Final
import voluptuous as vol
from xknx import XKNX
from xknx.core.telegram_queue import TelegramQueue
from xknx.dpt import DPTArray, DPTBase, DPTBinary
from xknx.exceptions import XKNXException
-from xknx.io import (
- DEFAULT_MCAST_GRP,
- DEFAULT_MCAST_PORT,
- ConnectionConfig,
- ConnectionType,
-)
+from xknx.io import ConnectionConfig, ConnectionType
from xknx.telegram import AddressFilter, Telegram
from xknx.telegram.address import parse_device_group_address
from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite
@@ -34,7 +30,15 @@ from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
-from .const import DOMAIN, KNX_ADDRESS, SupportedPlatforms
+from .const import (
+ CONF_KNX_EXPOSE,
+ CONF_KNX_INDIVIDUAL_ADDRESS,
+ CONF_KNX_ROUTING,
+ CONF_KNX_TUNNELING,
+ DOMAIN,
+ KNX_ADDRESS,
+ SupportedPlatforms,
+)
from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure
from .schema import (
BinarySensorSchema,
@@ -45,35 +49,29 @@ from .schema import (
FanSchema,
LightSchema,
NotifySchema,
+ NumberSchema,
SceneSchema,
+ SelectSchema,
SensorSchema,
SwitchSchema,
WeatherSchema,
ga_validator,
- ia_validator,
sensor_type_validator,
)
_LOGGER = logging.getLogger(__name__)
-CONF_KNX_ROUTING = "routing"
-CONF_KNX_TUNNELING = "tunneling"
-CONF_KNX_FIRE_EVENT = "fire_event"
-CONF_KNX_EVENT_FILTER = "event_filter"
-CONF_KNX_INDIVIDUAL_ADDRESS = "individual_address"
-CONF_KNX_MCAST_GRP = "multicast_group"
-CONF_KNX_MCAST_PORT = "multicast_port"
-CONF_KNX_STATE_UPDATER = "state_updater"
-CONF_KNX_RATE_LIMIT = "rate_limit"
-CONF_KNX_EXPOSE = "expose"
-SERVICE_KNX_SEND = "send"
-SERVICE_KNX_ATTR_PAYLOAD = "payload"
-SERVICE_KNX_ATTR_TYPE = "type"
-SERVICE_KNX_ATTR_REMOVE = "remove"
-SERVICE_KNX_EVENT_REGISTER = "event_register"
-SERVICE_KNX_EXPOSURE_REGISTER = "exposure_register"
-SERVICE_KNX_READ = "read"
+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"
+SERVICE_KNX_ATTR_REMOVE: Final = "remove"
+SERVICE_KNX_EVENT_REGISTER: Final = "event_register"
+SERVICE_KNX_EXPOSURE_REGISTER: Final = "exposure_register"
+SERVICE_KNX_READ: Final = "read"
CONFIG_SCHEMA = vol.Schema(
{
@@ -85,62 +83,24 @@ CONFIG_SCHEMA = vol.Schema(
cv.deprecated("fire_event_filter", replacement_key=CONF_KNX_EVENT_FILTER),
vol.Schema(
{
- vol.Exclusive(
- CONF_KNX_ROUTING, "connection_type"
- ): ConnectionSchema.ROUTING_SCHEMA,
- vol.Exclusive(
- CONF_KNX_TUNNELING, "connection_type"
- ): ConnectionSchema.TUNNELING_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]
),
- 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=True): cv.boolean,
- vol.Optional(CONF_KNX_RATE_LIMIT, default=20): vol.All(
- vol.Coerce(int), vol.Range(min=1, max=100)
- ),
- vol.Optional(CONF_KNX_EXPOSE): vol.All(
- cv.ensure_list, [ExposeSchema.SCHEMA]
- ),
- vol.Optional(SupportedPlatforms.COVER.value): vol.All(
- cv.ensure_list, [CoverSchema.SCHEMA]
- ),
- vol.Optional(SupportedPlatforms.BINARY_SENSOR.value): vol.All(
- cv.ensure_list, [BinarySensorSchema.SCHEMA]
- ),
- vol.Optional(SupportedPlatforms.LIGHT.value): vol.All(
- cv.ensure_list, [LightSchema.SCHEMA]
- ),
- vol.Optional(SupportedPlatforms.CLIMATE.value): vol.All(
- cv.ensure_list, [ClimateSchema.SCHEMA]
- ),
- vol.Optional(SupportedPlatforms.NOTIFY.value): vol.All(
- cv.ensure_list, [NotifySchema.SCHEMA]
- ),
- vol.Optional(SupportedPlatforms.SWITCH.value): vol.All(
- cv.ensure_list, [SwitchSchema.SCHEMA]
- ),
- vol.Optional(SupportedPlatforms.SENSOR.value): vol.All(
- cv.ensure_list, [SensorSchema.SCHEMA]
- ),
- vol.Optional(SupportedPlatforms.SCENE.value): vol.All(
- cv.ensure_list, [SceneSchema.SCHEMA]
- ),
- vol.Optional(SupportedPlatforms.WEATHER.value): vol.All(
- cv.ensure_list, [WeatherSchema.SCHEMA]
- ),
- vol.Optional(SupportedPlatforms.FAN.value): vol.All(
- cv.ensure_list, [FanSchema.SCHEMA]
- ),
+ **ExposeSchema.platform_node(),
+ **BinarySensorSchema.platform_node(),
+ **ClimateSchema.platform_node(),
+ **CoverSchema.platform_node(),
+ **FanSchema.platform_node(),
+ **LightSchema.platform_node(),
+ **NotifySchema.platform_node(),
+ **NumberSchema.platform_node(),
+ **SceneSchema.platform_node(),
+ **SelectSchema.platform_node(),
+ **SensorSchema.platform_node(),
+ **SwitchSchema.platform_node(),
+ **WeatherSchema.platform_node(),
}
),
)
@@ -315,11 +275,11 @@ class KNXModule:
"""Initialize XKNX object."""
self.xknx = XKNX(
own_address=self.config[DOMAIN][CONF_KNX_INDIVIDUAL_ADDRESS],
- rate_limit=self.config[DOMAIN][CONF_KNX_RATE_LIMIT],
- multicast_group=self.config[DOMAIN][CONF_KNX_MCAST_GRP],
- multicast_port=self.config[DOMAIN][CONF_KNX_MCAST_PORT],
+ rate_limit=self.config[DOMAIN][ConnectionSchema.CONF_KNX_RATE_LIMIT],
+ multicast_group=self.config[DOMAIN][ConnectionSchema.CONF_KNX_MCAST_GRP],
+ multicast_port=self.config[DOMAIN][ConnectionSchema.CONF_KNX_MCAST_PORT],
connection_config=self.connection_config(),
- state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER],
+ state_updater=self.config[DOMAIN][ConnectionSchema.CONF_KNX_STATE_UPDATER],
)
async def start(self) -> None:
@@ -338,7 +298,6 @@ class KNXModule:
return self.connection_config_tunneling()
if CONF_KNX_ROUTING in self.config[DOMAIN]:
return self.connection_config_routing()
- # config from xknx.yaml always has priority later on
return ConnectionConfig(auto_reconnect=True)
def connection_config_routing(self) -> ConnectionConfig:
@@ -419,14 +378,16 @@ class KNXModule:
"Service event_register could not remove event for '%s'",
str(group_address),
)
- else:
- for group_address in group_addresses:
- if group_address not in self._knx_event_callback.group_addresses:
- self._knx_event_callback.group_addresses.append(group_address)
- _LOGGER.debug(
- "Service event_register registered event for '%s'",
- str(group_address),
- )
+ return
+
+ for group_address in group_addresses:
+ if group_address in self._knx_event_callback.group_addresses:
+ continue
+ self._knx_event_callback.group_addresses.append(group_address)
+ _LOGGER.debug(
+ "Service event_register registered event for '%s'",
+ str(group_address),
+ )
async def service_exposure_register_modify(self, call: ServiceCall) -> None:
"""Service for adding or removing an exposure to KNX bus."""
@@ -445,7 +406,6 @@ class KNXModule:
if group_address in self.service_exposures:
replaced_exposure = self.service_exposures.pop(group_address)
- assert replaced_exposure.device is not None
_LOGGER.warning(
"Service exposure_register replacing already registered exposure for '%s' - %s",
group_address,
diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py
index a3271e605af..9a9c9627670 100644
--- a/homeassistant/components/knx/binary_sensor.py
+++ b/homeassistant/components/knx/binary_sensor.py
@@ -31,19 +31,18 @@ async def async_setup_platform(
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx
- entities = []
- for entity_config in platform_config:
- entities.append(KNXBinarySensor(xknx, entity_config))
-
- async_add_entities(entities)
+ async_add_entities(
+ KNXBinarySensor(xknx, entity_config) for entity_config in platform_config
+ )
class KNXBinarySensor(KnxEntity, BinarySensorEntity):
"""Representation of a KNX binary sensor."""
+ _device: XknxBinarySensor
+
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize of KNX binary sensor."""
- self._device: XknxBinarySensor
super().__init__(
device=XknxBinarySensor(
xknx,
@@ -58,13 +57,9 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity):
reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER),
)
)
- self._device_class: str | None = config.get(CONF_DEVICE_CLASS)
- self._unique_id = f"{self._device.remote_value.group_address_state}"
-
- @property
- def device_class(self) -> str | None:
- """Return the class of this sensor."""
- return self._device_class
+ self._attr_device_class = config.get(CONF_DEVICE_CLASS)
+ self._attr_force_update = self._device.ignore_internal_state
+ self._attr_unique_id = str(self._device.remote_value.group_address_state)
@property
def is_on(self) -> bool:
@@ -84,13 +79,3 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity):
dt.as_utc(self._device.last_telegram.timestamp)
)
return attr
-
- @property
- def force_update(self) -> bool:
- """
- Return True if state updates should be forced.
-
- If True, a state change will be triggered anytime the state property is
- updated, not just when the value changes.
- """
- return self._device.ignore_internal_state
diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py
index b8c07767005..c2e2a269b27 100644
--- a/homeassistant/components/knx/climate.py
+++ b/homeassistant/components/knx/climate.py
@@ -10,6 +10,8 @@ from xknx.telegram.address import parse_device_group_address
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
+ CURRENT_HVAC_IDLE,
+ CURRENT_HVAC_OFF,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
PRESET_AWAY,
@@ -22,10 +24,11 @@ from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
-from .const import CONTROLLER_MODES, DOMAIN, PRESET_MODES
+from .const import CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, DOMAIN, PRESET_MODES
from .knx_entity import KnxEntity
from .schema import ClimateSchema
+ATTR_COMMAND_VALUE = "command_value"
CONTROLLER_MODES_INV = {value: key for key, value in CONTROLLER_MODES.items()}
PRESET_MODES_INV = {value: key for key, value in PRESET_MODES.items()}
@@ -44,11 +47,9 @@ async def async_setup_platform(
xknx: XKNX = hass.data[DOMAIN].xknx
_async_migrate_unique_id(hass, platform_config)
- entities = []
- for entity_config in platform_config:
- entities.append(KNXClimate(xknx, entity_config))
-
- async_add_entities(entities)
+ async_add_entities(
+ KNXClimate(xknx, entity_config) for entity_config in platform_config
+ )
@callback
@@ -156,32 +157,36 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
temperature_step=config[ClimateSchema.CONF_TEMPERATURE_STEP],
group_address_on_off=config.get(ClimateSchema.CONF_ON_OFF_ADDRESS),
group_address_on_off_state=config.get(ClimateSchema.CONF_ON_OFF_STATE_ADDRESS),
+ on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT],
+ group_address_active_state=config.get(ClimateSchema.CONF_ACTIVE_STATE_ADDRESS),
+ group_address_command_value_state=config.get(
+ ClimateSchema.CONF_COMMAND_VALUE_STATE_ADDRESS
+ ),
min_temp=config.get(ClimateSchema.CONF_MIN_TEMP),
max_temp=config.get(ClimateSchema.CONF_MAX_TEMP),
mode=climate_mode,
- on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT],
)
class KNXClimate(KnxEntity, ClimateEntity):
"""Representation of a KNX climate device."""
+ _device: XknxClimate
+ _attr_temperature_unit = TEMP_CELSIUS
+
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize of a KNX climate device."""
- self._device: XknxClimate
super().__init__(_create_climate(xknx, config))
- self._unique_id = (
+ self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE
+ if self.preset_modes:
+ self._attr_supported_features |= SUPPORT_PRESET_MODE
+ self._attr_target_temperature_step = self._device.temperature_step
+ self._attr_unique_id = (
f"{self._device.temperature.group_address_state}_"
f"{self._device.target_temperature.group_address_state}_"
f"{self._device.target_temperature.group_address}_"
f"{self._device._setpoint_shift.group_address}" # pylint: disable=protected-access
)
- self._unit_of_measurement = TEMP_CELSIUS
-
- @property
- def supported_features(self) -> int:
- """Return the list of supported features."""
- return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
async def async_update(self) -> None:
"""Request a state update from KNX bus."""
@@ -189,21 +194,11 @@ class KNXClimate(KnxEntity, ClimateEntity):
if self._device.mode is not None:
await self._device.mode.sync()
- @property
- def temperature_unit(self) -> str:
- """Return the unit of measurement."""
- return self._unit_of_measurement
-
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._device.temperature.value
- @property
- def target_temperature_step(self) -> float:
- """Return the supported step of target temperature."""
- return self._device.temperature_step
-
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
@@ -260,6 +255,22 @@ class KNXClimate(KnxEntity, ClimateEntity):
# default to ["heat"]
return hvac_modes if hvac_modes else [HVAC_MODE_HEAT]
+ @property
+ def hvac_action(self) -> str | None:
+ """Return the current running hvac operation if supported.
+
+ Need to be one of CURRENT_HVAC_*.
+ """
+ if self._device.supports_on_off and not self._device.is_on:
+ return CURRENT_HVAC_OFF
+ if self._device.is_active is False:
+ return CURRENT_HVAC_IDLE
+ if self._device.mode is not None and self._device.mode.supports_controller_mode:
+ return CURRENT_HVAC_ACTIONS.get(
+ self._device.mode.controller_mode.value, CURRENT_HVAC_IDLE
+ )
+ return None
+
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set operation mode."""
if self._device.supports_on_off and hvac_mode == HVAC_MODE_OFF:
@@ -308,3 +319,24 @@ class KNXClimate(KnxEntity, ClimateEntity):
knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode))
await self._device.mode.set_operation_mode(knx_operation_mode)
self.async_write_ha_state()
+
+ @property
+ def extra_state_attributes(self) -> dict[str, Any] | None:
+ """Return device specific state attributes."""
+ attr: dict[str, Any] = {}
+
+ if self._device.command_value.initialized:
+ attr[ATTR_COMMAND_VALUE] = self._device.command_value.value
+ return attr
+
+ async def async_added_to_hass(self) -> None:
+ """Store register state change callback."""
+ await super().async_added_to_hass()
+ if self._device.mode is not None:
+ self._device.mode.register_device_updated_cb(self.after_update_callback)
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Disconnect device object when removed."""
+ await super().async_will_remove_from_hass()
+ if self._device.mode is not None:
+ self._device.mode.unregister_device_updated_cb(self.after_update_callback)
diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py
index 78b3f5ec7f9..421297da9d6 100644
--- a/homeassistant/components/knx/const.py
+++ b/homeassistant/components/knx/const.py
@@ -1,7 +1,13 @@
"""Constants for the KNX integration."""
from enum import Enum
+from typing import Final
from homeassistant.components.climate.const import (
+ CURRENT_HVAC_COOL,
+ CURRENT_HVAC_DRY,
+ CURRENT_HVAC_FAN,
+ CURRENT_HVAC_HEAT,
+ CURRENT_HVAC_OFF,
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
@@ -15,19 +21,24 @@ from homeassistant.components.climate.const import (
PRESET_SLEEP,
)
-DOMAIN = "knx"
+DOMAIN: Final = "knx"
# Address is used for configuration and services by the same functions so the key has to match
-KNX_ADDRESS = "address"
+KNX_ADDRESS: Final = "address"
-CONF_INVERT = "invert"
-CONF_STATE_ADDRESS = "state_address"
-CONF_SYNC_STATE = "sync_state"
-CONF_RESET_AFTER = "reset_after"
+CONF_INVERT: Final = "invert"
+CONF_KNX_EXPOSE: Final = "expose"
+CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address"
+CONF_KNX_ROUTING: Final = "routing"
+CONF_KNX_TUNNELING: Final = "tunneling"
+CONF_RESET_AFTER: Final = "reset_after"
+CONF_RESPOND_TO_READ: Final = "respond_to_read"
+CONF_STATE_ADDRESS: Final = "state_address"
+CONF_SYNC_STATE: Final = "sync_state"
-ATTR_COUNTER = "counter"
-ATTR_SOURCE = "source"
-ATTR_LAST_KNX_UPDATE = "last_knx_update"
+ATTR_COUNTER: Final = "counter"
+ATTR_LAST_KNX_UPDATE: Final = "last_knx_update"
+ATTR_SOURCE: Final = "source"
class ColorTempModes(Enum):
@@ -46,14 +57,16 @@ class SupportedPlatforms(Enum):
FAN = "fan"
LIGHT = "light"
NOTIFY = "notify"
+ NUMBER = "number"
SCENE = "scene"
+ SELECT = "select"
SENSOR = "sensor"
SWITCH = "switch"
WEATHER = "weather"
# Map KNX controller modes to HA modes. This list might not be complete.
-CONTROLLER_MODES = {
+CONTROLLER_MODES: Final = {
# Map DPT 20.105 HVAC control modes
"Auto": HVAC_MODE_AUTO,
"Heat": HVAC_MODE_HEAT,
@@ -63,7 +76,15 @@ CONTROLLER_MODES = {
"Dry": HVAC_MODE_DRY,
}
-PRESET_MODES = {
+CURRENT_HVAC_ACTIONS: Final = {
+ "Heat": CURRENT_HVAC_HEAT,
+ "Cool": CURRENT_HVAC_COOL,
+ "Off": CURRENT_HVAC_OFF,
+ "Fan only": CURRENT_HVAC_FAN,
+ "Dry": CURRENT_HVAC_DRY,
+}
+
+PRESET_MODES: Final = {
# Map DPT 20.102 HVAC operating modes to HA presets
"Auto": PRESET_NONE,
"Frost Protection": PRESET_ECO,
diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py
index 58d627ecb5e..5d32726474c 100644
--- a/homeassistant/components/knx/cover.py
+++ b/homeassistant/components/knx/cover.py
@@ -44,15 +44,12 @@ async def async_setup_platform(
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
- _async_migrate_unique_id(hass, platform_config)
-
xknx: XKNX = hass.data[DOMAIN].xknx
- entities = []
- for entity_config in platform_config:
- entities.append(KNXCover(xknx, entity_config))
-
- async_add_entities(entities)
+ _async_migrate_unique_id(hass, platform_config)
+ async_add_entities(
+ KNXCover(xknx, entity_config) for entity_config in platform_config
+ )
@callback
@@ -85,9 +82,10 @@ def _async_migrate_unique_id(
class KNXCover(KnxEntity, CoverEntity):
"""Representation of a KNX cover."""
+ _device: XknxCover
+
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize the cover."""
- self._device: XknxCover
super().__init__(
device=XknxCover(
xknx,
@@ -109,12 +107,26 @@ class KNXCover(KnxEntity, CoverEntity):
invert_angle=config[CoverSchema.CONF_INVERT_ANGLE],
)
)
- self._device_class: str | None = config.get(CONF_DEVICE_CLASS)
- self._unique_id = (
+ self._unsubscribe_auto_updater: Callable[[], None] | None = None
+
+ self._attr_device_class = config.get(CONF_DEVICE_CLASS) or (
+ DEVICE_CLASS_BLIND if self._device.supports_angle else None
+ )
+ self._attr_supported_features = (
+ SUPPORT_CLOSE | SUPPORT_OPEN | SUPPORT_SET_POSITION
+ )
+ if self._device.supports_stop:
+ self._attr_supported_features |= SUPPORT_STOP | SUPPORT_STOP_TILT
+ if self._device.supports_angle:
+ self._attr_supported_features |= SUPPORT_SET_TILT_POSITION
+ if self._device.step.writable:
+ self._attr_supported_features |= (
+ SUPPORT_CLOSE_TILT | SUPPORT_OPEN_TILT | SUPPORT_STOP_TILT
+ )
+ self._attr_unique_id = (
f"{self._device.updown.group_address}_"
f"{self._device.position_target.group_address}"
)
- self._unsubscribe_auto_updater: Callable[[], None] | None = None
@callback
async def after_update_callback(self, device: XknxDevice) -> None:
@@ -123,30 +135,6 @@ class KNXCover(KnxEntity, CoverEntity):
if self._device.is_traveling():
self.start_auto_updater()
- @property
- def device_class(self) -> str | None:
- """Return the class of this device, from component DEVICE_CLASSES."""
- if self._device_class:
- return self._device_class
- if self._device.supports_angle:
- return DEVICE_CLASS_BLIND
- return None
-
- @property
- def supported_features(self) -> int:
- """Flag supported features."""
- supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
- if self._device.supports_stop:
- supported_features |= SUPPORT_STOP
- if self._device.supports_angle:
- supported_features |= (
- SUPPORT_SET_TILT_POSITION
- | SUPPORT_OPEN_TILT
- | SUPPORT_CLOSE_TILT
- | SUPPORT_STOP_TILT
- )
- return supported_features
-
@property
def current_cover_position(self) -> int | None:
"""Return the current position of the cover.
diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py
index 5c371445cc4..5b57e2b0b4c 100644
--- a/homeassistant/components/knx/expose.py
+++ b/homeassistant/components/knx/expose.py
@@ -5,6 +5,8 @@ from typing import Callable
from xknx import XKNX
from xknx.devices import DateTime, ExposeSensor
+from xknx.dpt import DPTNumeric
+from xknx.remote_value import RemoteValueSensor
from homeassistant.const import (
CONF_ENTITY_ID,
@@ -122,9 +124,15 @@ class KNXExposeSensor:
)
if self.type == "binary":
if value in (1, STATE_ON, "True"):
- value = True
- elif value in (0, STATE_OFF, "False"):
- value = False
+ return True
+ if value in (0, STATE_OFF, "False"):
+ return False
+ if (
+ value is not None
+ and isinstance(self.device.sensor_value, RemoteValueSensor)
+ and issubclass(self.device.sensor_value.dpt_class, DPTNumeric)
+ ):
+ return float(value)
return value
async def _async_entity_changed(self, event: Event) -> None:
diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py
index 4b4a84c26d2..f787795e1e8 100644
--- a/homeassistant/components/knx/fan.py
+++ b/homeassistant/components/knx/fan.py
@@ -2,7 +2,7 @@
from __future__ import annotations
import math
-from typing import Any
+from typing import Any, Final
from xknx import XKNX
from xknx.devices import Fan as XknxFan
@@ -22,7 +22,7 @@ from .const import DOMAIN, KNX_ADDRESS
from .knx_entity import KnxEntity
from .schema import FanSchema
-DEFAULT_PERCENTAGE = 50
+DEFAULT_PERCENTAGE: Final = 50
async def async_setup_platform(
@@ -34,23 +34,19 @@ async def async_setup_platform(
"""Set up fans for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
-
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx
- entities = []
- for entity_config in platform_config:
- entities.append(KNXFan(xknx, entity_config))
-
- async_add_entities(entities)
+ async_add_entities(KNXFan(xknx, entity_config) for entity_config in platform_config)
class KNXFan(KnxEntity, FanEntity):
"""Representation of a KNX fan."""
+ _device: XknxFan
+
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize of KNX fan."""
- self._device: XknxFan
max_step = config.get(FanSchema.CONF_MAX_STEP)
super().__init__(
device=XknxFan(
@@ -67,10 +63,14 @@ class KNXFan(KnxEntity, FanEntity):
max_step=max_step,
)
)
- self._unique_id = f"{self._device.speed.group_address}"
# FanSpeedMode.STEP if max_step is set
self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None
+ self._attr_supported_features = SUPPORT_SET_SPEED
+ if self._device.supports_oscillation:
+ self._attr_supported_features |= SUPPORT_OSCILLATE
+ self._attr_unique_id = str(self._device.speed.group_address)
+
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
if self._step_range:
@@ -79,16 +79,6 @@ class KNXFan(KnxEntity, FanEntity):
else:
await self._device.set_speed(percentage)
- @property
- def supported_features(self) -> int:
- """Flag supported features."""
- flags = SUPPORT_SET_SPEED
-
- if self._device.supports_oscillation:
- flags |= SUPPORT_OSCILLATE
-
- return flags
-
@property
def percentage(self) -> int | None:
"""Return the current speed as a percentage."""
diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py
index 1e374250bba..5f2e14d1466 100644
--- a/homeassistant/components/knx/knx_entity.py
+++ b/homeassistant/components/knx/knx_entity.py
@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import cast
-from xknx.devices import Climate as XknxClimate, Device as XknxDevice
+from xknx.devices import Device as XknxDevice
from homeassistant.helpers.entity import Entity
@@ -14,10 +14,11 @@ from .const import DOMAIN
class KnxEntity(Entity):
"""Representation of a KNX entity."""
+ _attr_should_poll = False
+
def __init__(self, device: XknxDevice) -> None:
"""Set up device."""
self._device = device
- self._unique_id: str | None = None
@property
def name(self) -> str:
@@ -30,16 +31,6 @@ class KnxEntity(Entity):
knx_module = cast(KNXModule, self.hass.data[DOMAIN])
return knx_module.connected
- @property
- def should_poll(self) -> bool:
- """No polling needed within KNX."""
- return False
-
- @property
- def unique_id(self) -> str | None:
- """Return the unique id of the device."""
- return self._unique_id
-
async def async_update(self) -> None:
"""Request a state update from KNX bus."""
await self._device.sync()
@@ -52,12 +43,6 @@ class KnxEntity(Entity):
"""Store register state change callback."""
self._device.register_device_updated_cb(self.after_update_callback)
- if isinstance(self._device, XknxClimate) and self._device.mode is not None:
- self._device.mode.register_device_updated_cb(self.after_update_callback)
-
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
self._device.unregister_device_updated_cb(self.after_update_callback)
-
- if isinstance(self._device, XknxClimate) and self._device.mode is not None:
- self._device.mode.unregister_device_updated_cb(self.after_update_callback)
diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py
index ed4abac63b5..56068b5deae 100644
--- a/homeassistant/components/knx/light.py
+++ b/homeassistant/components/knx/light.py
@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any, Tuple, cast
from xknx import XKNX
-from xknx.devices import Light as XknxLight
+from xknx.devices.light import Light as XknxLight, XYYColor
from xknx.telegram.address import parse_device_group_address
from homeassistant.components.light import (
@@ -12,11 +12,13 @@ from homeassistant.components.light import (
ATTR_COLOR_TEMP,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
+ ATTR_XY_COLOR,
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_ONOFF,
COLOR_MODE_RGB,
COLOR_MODE_RGBW,
+ COLOR_MODE_XY,
LightEntity,
)
from homeassistant.const import CONF_NAME
@@ -41,15 +43,12 @@ async def async_setup_platform(
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
- _async_migrate_unique_id(hass, platform_config)
-
xknx: XKNX = hass.data[DOMAIN].xknx
- entities = []
- for entity_config in platform_config:
- entities.append(KNXLight(xknx, entity_config))
-
- async_add_entities(entities)
+ _async_migrate_unique_id(hass, platform_config)
+ async_add_entities(
+ KNXLight(xknx, entity_config) for entity_config in platform_config
+ )
@callback
@@ -159,6 +158,8 @@ def _create_light(xknx: XKNX, config: ConfigType) -> XknxLight:
group_address_color_state=config.get(LightSchema.CONF_COLOR_STATE_ADDRESS),
group_address_rgbw=config.get(LightSchema.CONF_RGBW_ADDRESS),
group_address_rgbw_state=config.get(LightSchema.CONF_RGBW_STATE_ADDRESS),
+ group_address_xyy_color=config.get(LightSchema.CONF_XYY_ADDRESS),
+ group_address_xyy_color_state=config.get(LightSchema.CONF_XYY_STATE_ADDRESS),
group_address_tunable_white=group_address_tunable_white,
group_address_tunable_white_state=group_address_tunable_white_state,
group_address_color_temperature=group_address_color_temp,
@@ -219,19 +220,21 @@ def _create_light(xknx: XKNX, config: ConfigType) -> XknxLight:
class KNXLight(KnxEntity, LightEntity):
"""Representation of a KNX light."""
+ _device: XknxLight
+
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize of KNX light."""
- self._device: XknxLight
super().__init__(_create_light(xknx, config))
- self._unique_id = self._device_unique_id()
- self._min_kelvin: int = config[LightSchema.CONF_MIN_KELVIN]
self._max_kelvin: int = config[LightSchema.CONF_MAX_KELVIN]
- self._min_mireds = color_util.color_temperature_kelvin_to_mired(
- self._max_kelvin
- )
- self._max_mireds = color_util.color_temperature_kelvin_to_mired(
+ self._min_kelvin: int = config[LightSchema.CONF_MIN_KELVIN]
+
+ self._attr_max_mireds = color_util.color_temperature_kelvin_to_mired(
self._min_kelvin
)
+ self._attr_min_mireds = color_util.color_temperature_kelvin_to_mired(
+ self._max_kelvin
+ )
+ self._attr_unique_id = self._device_unique_id()
def _device_unique_id(self) -> str:
"""Return unique id for this device."""
@@ -253,6 +256,9 @@ class KNXLight(KnxEntity, LightEntity):
"""Return the brightness of this light between 0..255."""
if self._device.supports_brightness:
return self._device.current_brightness
+ if self._device.current_xyy_color is not None:
+ _, brightness = self._device.current_xyy_color
+ return brightness
if (rgb := self.rgb_color) is not None:
return max(rgb)
return None
@@ -277,6 +283,14 @@ class KNXLight(KnxEntity, LightEntity):
return (*rgb, white)
return None
+ @property
+ def xy_color(self) -> tuple[float, float] | None:
+ """Return the xy color value [float, float]."""
+ if self._device.current_xyy_color is not None:
+ xy_color, _ = self._device.current_xyy_color
+ return xy_color
+ return None
+
@property
def color_temp(self) -> int | None:
"""Return the color temperature in mireds."""
@@ -296,19 +310,11 @@ class KNXLight(KnxEntity, LightEntity):
)
return None
- @property
- def min_mireds(self) -> int:
- """Return the coldest color temp this light supports in mireds."""
- return self._min_mireds
-
- @property
- def max_mireds(self) -> int:
- """Return the warmest color temp this light supports in mireds."""
- return self._max_mireds
-
@property
def color_mode(self) -> str | None:
"""Return the color mode of the light."""
+ if self._device.supports_xyy_color:
+ return COLOR_MODE_XY
if self._device.supports_rgbw:
return COLOR_MODE_RGBW
if self._device.supports_color:
@@ -329,22 +335,11 @@ class KNXLight(KnxEntity, LightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
- # ignore arguments if not supported to fall back to set_on()
- brightness = (
- kwargs.get(ATTR_BRIGHTNESS)
- if self._device.supports_brightness
- or self.color_mode in (COLOR_MODE_RGB, COLOR_MODE_RGBW)
- else None
- )
- mireds = (
- kwargs.get(ATTR_COLOR_TEMP)
- if self.color_mode == COLOR_MODE_COLOR_TEMP
- else None
- )
- rgb = kwargs.get(ATTR_RGB_COLOR) if self.color_mode == COLOR_MODE_RGB else None
- rgbw = (
- kwargs.get(ATTR_RGBW_COLOR) if self.color_mode == COLOR_MODE_RGBW else None
- )
+ brightness = kwargs.get(ATTR_BRIGHTNESS)
+ mireds = kwargs.get(ATTR_COLOR_TEMP)
+ rgb = kwargs.get(ATTR_RGB_COLOR)
+ rgbw = kwargs.get(ATTR_RGBW_COLOR)
+ xy_color = kwargs.get(ATTR_XY_COLOR)
if (
not self.is_on
@@ -352,6 +347,7 @@ class KNXLight(KnxEntity, LightEntity):
and mireds is None
and rgb is None
and rgbw is None
+ and xy_color is None
):
await self._device.set_on()
return
@@ -394,12 +390,22 @@ class KNXLight(KnxEntity, LightEntity):
)
await self._device.set_tunable_white(relative_ct)
+ if xy_color is not None:
+ await self._device.set_xyy_color(
+ XYYColor(color=xy_color, brightness=brightness)
+ )
+ return
+
if brightness is not None:
# brightness: 1..255; 0 brightness will call async_turn_off()
if self._device.brightness.writable:
await self._device.set_brightness(brightness)
return
- # brightness without color in kwargs; set via color - default to white
+ # brightness without color in kwargs; set via color
+ if self.color_mode == COLOR_MODE_XY:
+ await self._device.set_xyy_color(XYYColor(brightness=brightness))
+ return
+ # default to white if color not known for RGB(W)
if self.color_mode == COLOR_MODE_RGBW:
rgbw = self.rgbw_color
if not rgbw or not any(rgbw):
diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json
index 6b1d4d328ac..e075411d8b8 100644
--- a/homeassistant/components/knx/manifest.json
+++ b/homeassistant/components/knx/manifest.json
@@ -2,7 +2,7 @@
"domain": "knx",
"name": "KNX",
"documentation": "https://www.home-assistant.io/integrations/knx",
- "requirements": ["xknx==0.18.4"],
+ "requirements": ["xknx==0.18.8"],
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
"quality_scale": "silver",
"iot_class": "local_push"
diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py
new file mode 100644
index 00000000000..eb4f21de513
--- /dev/null
+++ b/homeassistant/components/knx/number.py
@@ -0,0 +1,95 @@
+"""Support for KNX/IP numeric values."""
+from __future__ import annotations
+
+from typing import cast
+
+from xknx import XKNX
+from xknx.devices import NumericValue
+
+from homeassistant.components.number import NumberEntity
+from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_UNAVAILABLE, STATE_UNKNOWN
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.restore_state import RestoreEntity
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+
+from .const import CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, DOMAIN, KNX_ADDRESS
+from .knx_entity import KnxEntity
+from .schema import NumberSchema
+
+
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
+ """Set up number entities for KNX platform."""
+ if not discovery_info or not discovery_info["platform_config"]:
+ return
+ platform_config = discovery_info["platform_config"]
+ xknx: XKNX = hass.data[DOMAIN].xknx
+
+ async_add_entities(
+ KNXNumber(xknx, entity_config) for entity_config in platform_config
+ )
+
+
+def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue:
+ """Return a KNX NumericValue to be used within XKNX."""
+ return NumericValue(
+ xknx,
+ name=config[CONF_NAME],
+ group_address=config[KNX_ADDRESS],
+ group_address_state=config.get(CONF_STATE_ADDRESS),
+ respond_to_read=config[CONF_RESPOND_TO_READ],
+ value_type=config[CONF_TYPE],
+ )
+
+
+class KNXNumber(KnxEntity, NumberEntity, RestoreEntity):
+ """Representation of a KNX number."""
+
+ _device: NumericValue
+
+ def __init__(self, xknx: XKNX, config: ConfigType) -> None:
+ """Initialize a KNX number."""
+ super().__init__(_create_numeric_value(xknx, config))
+ self._attr_max_value = config.get(
+ NumberSchema.CONF_MAX,
+ self._device.sensor_value.dpt_class.value_max,
+ )
+ self._attr_min_value = config.get(
+ NumberSchema.CONF_MIN,
+ self._device.sensor_value.dpt_class.value_min,
+ )
+ self._attr_step = config.get(
+ NumberSchema.CONF_STEP,
+ self._device.sensor_value.dpt_class.resolution,
+ )
+ self._attr_unique_id = str(self._device.sensor_value.group_address)
+ self._device.sensor_value.value = max(0, self._attr_min_value)
+
+ async def async_added_to_hass(self) -> None:
+ """Restore last state."""
+ await super().async_added_to_hass()
+ if not self._device.sensor_value.readable and (
+ last_state := await self.async_get_last_state()
+ ):
+ if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
+ self._device.sensor_value.value = float(last_state.state)
+
+ @property
+ def value(self) -> float:
+ """Return the entity value to represent the entity state."""
+ # self._device.sensor_value.value is set in __init__ so it is never None
+ return cast(float, self._device.resolve_state())
+
+ async def async_set_value(self, value: float) -> None:
+ """Set new value."""
+ if value < self.min_value or value > self.max_value:
+ raise ValueError(
+ f"Invalid value for {self.entity_id}: {value} "
+ f"(range {self.min_value} - {self.max_value})"
+ )
+ await self._device.set(value)
diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py
index e3b00815424..9bd32c99e41 100644
--- a/homeassistant/components/knx/scene.py
+++ b/homeassistant/components/knx/scene.py
@@ -26,23 +26,21 @@ async def async_setup_platform(
"""Set up the scenes for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
-
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx
- entities = []
- for entity_config in platform_config:
- entities.append(KNXScene(xknx, entity_config))
-
- async_add_entities(entities)
+ async_add_entities(
+ KNXScene(xknx, entity_config) for entity_config in platform_config
+ )
class KNXScene(KnxEntity, Scene):
"""Representation of a KNX scene."""
+ _device: XknxScene
+
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Init KNX scene."""
- self._device: XknxScene
super().__init__(
device=XknxScene(
xknx,
@@ -51,7 +49,7 @@ class KNXScene(KnxEntity, Scene):
scene_number=config[SceneSchema.CONF_SCENE_NUMBER],
)
)
- self._unique_id = (
+ self._attr_unique_id = (
f"{self._device.scene_value.group_address}_{self._device.scene_number}"
)
diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py
index 3ac0ec84e1d..079dc7363bf 100644
--- a/homeassistant/components/knx/schema.py
+++ b/homeassistant/components/knx/schema.py
@@ -1,13 +1,16 @@
"""Voluptuous schemas for the KNX integration."""
from __future__ import annotations
-from typing import Any
+from abc import ABC
+from collections import OrderedDict
+from typing import Any, ClassVar
import voluptuous as vol
+from xknx import XKNX
from xknx.devices.climate import SetpointShiftMode
-from xknx.dpt import DPTBase
+from xknx.dpt import DPTBase, DPTNumeric
from xknx.exceptions import CouldNotParseAddress
-from xknx.io import DEFAULT_MCAST_PORT
+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 (
@@ -26,13 +29,19 @@ import homeassistant.helpers.config_validation as cv
from .const import (
CONF_INVERT,
+ CONF_KNX_EXPOSE,
+ CONF_KNX_INDIVIDUAL_ADDRESS,
+ CONF_KNX_ROUTING,
+ CONF_KNX_TUNNELING,
CONF_RESET_AFTER,
+ CONF_RESPOND_TO_READ,
CONF_STATE_ADDRESS,
CONF_SYNC_STATE,
CONTROLLER_MODES,
KNX_ADDRESS,
PRESET_MODES,
ColorTempModes,
+ SupportedPlatforms,
)
##################
@@ -63,6 +72,77 @@ ia_validator = vol.Any(
)
+def number_limit_sub_validator(entity_config: OrderedDict) -> OrderedDict:
+ """Validate a number entity configurations dependent on configured value type."""
+ value_type = entity_config[CONF_TYPE]
+ min_config: float | None = entity_config.get(NumberSchema.CONF_MIN)
+ max_config: float | None = entity_config.get(NumberSchema.CONF_MAX)
+ step_config: float | None = entity_config.get(NumberSchema.CONF_STEP)
+ dpt_class = DPTNumeric.parse_transcoder(value_type)
+
+ if dpt_class is None:
+ raise vol.Invalid(f"'type: {value_type}' is not a valid numeric sensor type.")
+ # Inifinity is not supported by Home Assistant frontend so user defined
+ # config is required if if xknx DPTNumeric subclass defines it as limit.
+ if min_config is None and dpt_class.value_min == float("-inf"):
+ raise vol.Invalid(f"'min' key required for value type '{value_type}'")
+ if min_config is not None and min_config < dpt_class.value_min:
+ raise vol.Invalid(
+ f"'min: {min_config}' undercuts possible minimum"
+ f" of value type '{value_type}': {dpt_class.value_min}"
+ )
+
+ if max_config is None and dpt_class.value_max == float("inf"):
+ raise vol.Invalid(f"'max' key required for value type '{value_type}'")
+ if max_config is not None and max_config > dpt_class.value_max:
+ raise vol.Invalid(
+ f"'max: {max_config}' exceeds possible maximum"
+ f" of value type '{value_type}': {dpt_class.value_max}"
+ )
+
+ if step_config is not None and step_config < dpt_class.resolution:
+ raise vol.Invalid(
+ f"'step: {step_config}' undercuts possible minimum step"
+ f" of value type '{value_type}': {dpt_class.resolution}"
+ )
+
+ return entity_config
+
+
+def numeric_type_validator(value: Any) -> str | int:
+ """Validate that value is parsable as numeric sensor type."""
+ if isinstance(value, (str, int)) and DPTNumeric.parse_transcoder(value) is not None:
+ return value
+ raise vol.Invalid(f"value '{value}' is not a valid numeric sensor type.")
+
+
+def select_options_sub_validator(entity_config: OrderedDict) -> OrderedDict:
+ """Validate a select entity options configuration."""
+ options_seen = set()
+ payloads_seen = set()
+ payload_length = entity_config[SelectSchema.CONF_PAYLOAD_LENGTH]
+ if payload_length == 0:
+ max_payload = 0x3F
+ else:
+ max_payload = 256 ** payload_length - 1
+
+ for opt in entity_config[SelectSchema.CONF_OPTIONS]:
+ option = opt[SelectSchema.CONF_OPTION]
+ payload = opt[SelectSchema.CONF_PAYLOAD]
+ if payload > max_payload:
+ raise vol.Invalid(
+ f"'payload: {payload}' for 'option: {option}' exceeds possible"
+ f" maximum of 'payload_length: {payload_length}': {max_payload}"
+ )
+ if option in options_seen:
+ raise vol.Invalid(f"duplicate item for 'option' not allowed: {option}")
+ options_seen.add(option)
+ if payload in payloads_seen:
+ raise vol.Invalid(f"duplicate item for 'payload' not allowed: {payload}")
+ payloads_seen.add(payload)
+ return entity_config
+
+
def sensor_type_validator(value: Any) -> str | int:
"""Validate that value is parsable as sensor type."""
if isinstance(value, (str, int)) and DPTBase.parse_transcoder(value) is not None:
@@ -76,6 +156,7 @@ sync_state_validator = vol.Any(
cv.matches_regex(r"^(init|expire|every)( \d*)?$"),
)
+
##############
# CONNECTION
##############
@@ -85,7 +166,11 @@ class ConnectionSchema:
"""Voluptuous schema for KNX connection."""
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"
TUNNELING_SCHEMA = vol.Schema(
{
@@ -98,15 +183,47 @@ class ConnectionSchema:
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=True): cv.boolean,
+ vol.Optional(CONF_KNX_RATE_LIMIT, default=20): vol.All(
+ vol.Coerce(int), vol.Range(min=1, max=100)
+ ),
+ }
+
#############
# PLATFORMS
#############
-class BinarySensorSchema:
+class KNXPlatformSchema(ABC):
+ """Voluptuous schema for KNX platform entity configuration."""
+
+ PLATFORM_NAME: ClassVar[str]
+ ENTITY_SCHEMA: ClassVar[vol.Schema]
+
+ @classmethod
+ def platform_node(cls) -> dict[vol.Optional, vol.All]:
+ """Return a schema node for the platform."""
+ return {
+ vol.Optional(cls.PLATFORM_NAME): vol.All(
+ cv.ensure_list, [cls.ENTITY_SCHEMA]
+ )
+ }
+
+
+class BinarySensorSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX binary sensors."""
+ PLATFORM_NAME = SupportedPlatforms.BINARY_SENSOR.value
+
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
CONF_SYNC_STATE = CONF_SYNC_STATE
CONF_INVERT = CONF_INVERT
@@ -116,7 +233,7 @@ class BinarySensorSchema:
DEFAULT_NAME = "KNX Binary Sensor"
- SCHEMA = vol.All(
+ ENTITY_SCHEMA = vol.All(
# deprecated since September 2020
cv.deprecated("significant_bit"),
cv.deprecated("automation"),
@@ -137,9 +254,12 @@ class BinarySensorSchema:
)
-class ClimateSchema:
+class ClimateSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX climate devices."""
+ PLATFORM_NAME = SupportedPlatforms.CLIMATE.value
+
+ CONF_ACTIVE_STATE_ADDRESS = "active_state_address"
CONF_SETPOINT_SHIFT_ADDRESS = "setpoint_shift_address"
CONF_SETPOINT_SHIFT_STATE_ADDRESS = "setpoint_shift_state_address"
CONF_SETPOINT_SHIFT_MODE = "setpoint_shift_mode"
@@ -155,6 +275,7 @@ class ClimateSchema:
CONF_CONTROLLER_STATUS_STATE_ADDRESS = "controller_status_state_address"
CONF_CONTROLLER_MODE_ADDRESS = "controller_mode_address"
CONF_CONTROLLER_MODE_STATE_ADDRESS = "controller_mode_state_address"
+ CONF_COMMAND_VALUE_STATE_ADDRESS = "command_value_state_address"
CONF_HEAT_COOL_ADDRESS = "heat_cool_address"
CONF_HEAT_COOL_STATE_ADDRESS = "heat_cool_state_address"
CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = (
@@ -178,7 +299,7 @@ class ClimateSchema:
DEFAULT_TEMPERATURE_STEP = 0.1
DEFAULT_ON_OFF_INVERT = False
- SCHEMA = vol.All(
+ ENTITY_SCHEMA = vol.All(
# deprecated since September 2020
cv.deprecated("setpoint_shift_step", replacement_key=CONF_TEMPERATURE_STEP),
# deprecated since 2021.6
@@ -213,6 +334,8 @@ class ClimateSchema:
vol.Optional(CONF_SETPOINT_SHIFT_MODE): vol.Maybe(
vol.All(vol.Upper, cv.enum(SetpointShiftMode))
),
+ vol.Optional(CONF_ACTIVE_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_COMMAND_VALUE_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_OPERATION_MODE_ADDRESS): ga_list_validator,
vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): ga_list_validator,
@@ -245,9 +368,11 @@ class ClimateSchema:
)
-class CoverSchema:
+class CoverSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX covers."""
+ PLATFORM_NAME = SupportedPlatforms.COVER.value
+
CONF_MOVE_LONG_ADDRESS = "move_long_address"
CONF_MOVE_SHORT_ADDRESS = "move_short_address"
CONF_STOP_ADDRESS = "stop_address"
@@ -263,7 +388,7 @@ class CoverSchema:
DEFAULT_TRAVEL_TIME = 25
DEFAULT_NAME = "KNX Cover"
- SCHEMA = vol.All(
+ ENTITY_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(
@@ -297,9 +422,11 @@ class CoverSchema:
)
-class ExposeSchema:
+class ExposeSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX exposures."""
+ PLATFORM_NAME = CONF_KNX_EXPOSE
+
CONF_KNX_EXPOSE_TYPE = CONF_TYPE
CONF_KNX_EXPOSE_ATTRIBUTE = "attribute"
CONF_KNX_EXPOSE_BINARY = "binary"
@@ -329,12 +456,14 @@ class ExposeSchema:
vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all,
}
)
- SCHEMA = vol.Any(EXPOSE_SENSOR_SCHEMA, EXPOSE_TIME_SCHEMA)
+ ENTITY_SCHEMA = vol.Any(EXPOSE_SENSOR_SCHEMA, EXPOSE_TIME_SCHEMA)
-class FanSchema:
+class FanSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX fans."""
+ PLATFORM_NAME = SupportedPlatforms.FAN.value
+
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
CONF_OSCILLATION_ADDRESS = "oscillation_address"
CONF_OSCILLATION_STATE_ADDRESS = "oscillation_state_address"
@@ -342,7 +471,7 @@ class FanSchema:
DEFAULT_NAME = "KNX Fan"
- SCHEMA = vol.Schema(
+ ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(KNX_ADDRESS): ga_list_validator,
@@ -354,9 +483,11 @@ class FanSchema:
)
-class LightSchema:
+class LightSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX lights."""
+ PLATFORM_NAME = SupportedPlatforms.LIGHT.value
+
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
CONF_BRIGHTNESS_ADDRESS = "brightness_address"
CONF_BRIGHTNESS_STATE_ADDRESS = "brightness_state_address"
@@ -367,6 +498,8 @@ class LightSchema:
CONF_COLOR_TEMP_MODE = "color_temperature_mode"
CONF_RGBW_ADDRESS = "rgbw_address"
CONF_RGBW_STATE_ADDRESS = "rgbw_state_address"
+ CONF_XYY_ADDRESS = "xyy_address"
+ CONF_XYY_STATE_ADDRESS = "xyy_state_address"
CONF_MIN_KELVIN = "min_kelvin"
CONF_MAX_KELVIN = "max_kelvin"
@@ -390,7 +523,7 @@ class LightSchema:
}
)
- SCHEMA = vol.All(
+ ENTITY_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@@ -425,6 +558,8 @@ class LightSchema:
): vol.All(vol.Upper, cv.enum(ColorTempModes)),
vol.Exclusive(CONF_RGBW_ADDRESS, "color"): ga_list_validator,
vol.Optional(CONF_RGBW_STATE_ADDRESS): ga_list_validator,
+ vol.Exclusive(CONF_XYY_ADDRESS, "color"): ga_list_validator,
+ vol.Optional(CONF_XYY_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
@@ -452,12 +587,14 @@ class LightSchema:
)
-class NotifySchema:
+class NotifySchema(KNXPlatformSchema):
"""Voluptuous schema for KNX notifications."""
+ PLATFORM_NAME = SupportedPlatforms.NOTIFY.value
+
DEFAULT_NAME = "KNX Notify"
- SCHEMA = vol.Schema(
+ ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(KNX_ADDRESS): ga_validator,
@@ -465,13 +602,42 @@ class NotifySchema:
)
-class SceneSchema:
+class NumberSchema(KNXPlatformSchema):
+ """Voluptuous schema for KNX numbers."""
+
+ PLATFORM_NAME = SupportedPlatforms.NUMBER.value
+
+ CONF_MAX = "max"
+ CONF_MIN = "min"
+ CONF_STEP = "step"
+ DEFAULT_NAME = "KNX Number"
+
+ ENTITY_SCHEMA = vol.All(
+ vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
+ vol.Required(CONF_TYPE): numeric_type_validator,
+ vol.Required(KNX_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_MAX): vol.Coerce(float),
+ vol.Optional(CONF_MIN): vol.Coerce(float),
+ vol.Optional(CONF_STEP): cv.positive_float,
+ }
+ ),
+ number_limit_sub_validator,
+ )
+
+
+class SceneSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX scenes."""
+ PLATFORM_NAME = SupportedPlatforms.SCENE.value
+
CONF_SCENE_NUMBER = "scene_number"
DEFAULT_NAME = "KNX SCENE"
- SCHEMA = vol.Schema(
+ ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(KNX_ADDRESS): ga_list_validator,
@@ -482,15 +648,51 @@ class SceneSchema:
)
-class SensorSchema:
+class SelectSchema(KNXPlatformSchema):
+ """Voluptuous schema for KNX selects."""
+
+ PLATFORM_NAME = SupportedPlatforms.SELECT.value
+
+ CONF_OPTION = "option"
+ CONF_OPTIONS = "options"
+ CONF_PAYLOAD = "payload"
+ CONF_PAYLOAD_LENGTH = "payload_length"
+ DEFAULT_NAME = "KNX Select"
+
+ ENTITY_SCHEMA = vol.All(
+ vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
+ vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
+ vol.Required(CONF_PAYLOAD_LENGTH): vol.All(
+ vol.Coerce(int), vol.Range(min=0, max=14)
+ ),
+ vol.Required(CONF_OPTIONS): [
+ {
+ vol.Required(CONF_OPTION): vol.Coerce(str),
+ vol.Required(CONF_PAYLOAD): cv.positive_int,
+ }
+ ],
+ vol.Required(KNX_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
+ }
+ ),
+ select_options_sub_validator,
+ )
+
+
+class SensorSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX sensors."""
+ PLATFORM_NAME = SupportedPlatforms.SENSOR.value
+
CONF_ALWAYS_CALLBACK = "always_callback"
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
CONF_SYNC_STATE = CONF_SYNC_STATE
DEFAULT_NAME = "KNX Sensor"
- SCHEMA = vol.Schema(
+ ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
@@ -501,26 +703,31 @@ class SensorSchema:
)
-class SwitchSchema:
+class SwitchSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX switches."""
+ PLATFORM_NAME = SupportedPlatforms.SWITCH.value
+
CONF_INVERT = CONF_INVERT
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
DEFAULT_NAME = "KNX Switch"
- SCHEMA = vol.Schema(
+ ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_INVERT, default=False): cv.boolean,
+ vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
}
)
-class WeatherSchema:
+class WeatherSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX weather station."""
+ PLATFORM_NAME = SupportedPlatforms.WEATHER.value
+
CONF_SYNC_STATE = CONF_SYNC_STATE
CONF_KNX_TEMPERATURE_ADDRESS = "address_temperature"
CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS = "address_brightness_south"
@@ -538,7 +745,7 @@ class WeatherSchema:
DEFAULT_NAME = "KNX Weather Station"
- SCHEMA = vol.All(
+ ENTITY_SCHEMA = vol.All(
# deprecated since 2021.6
cv.deprecated("create_sensors"),
vol.Schema(
diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py
new file mode 100644
index 00000000000..07f74c04e4f
--- /dev/null
+++ b/homeassistant/components/knx/select.py
@@ -0,0 +1,103 @@
+"""Support for KNX/IP select entities."""
+from __future__ import annotations
+
+from xknx import XKNX
+from xknx.devices import Device as XknxDevice, RawValue
+
+from homeassistant.components.select import SelectEntity
+from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE, STATE_UNKNOWN
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.restore_state import RestoreEntity
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+
+from .const import (
+ CONF_RESPOND_TO_READ,
+ CONF_STATE_ADDRESS,
+ CONF_SYNC_STATE,
+ DOMAIN,
+ KNX_ADDRESS,
+)
+from .knx_entity import KnxEntity
+from .schema import SelectSchema
+
+
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
+ """Set up select entities for KNX platform."""
+ if not discovery_info or not discovery_info["platform_config"]:
+ return
+ platform_config = discovery_info["platform_config"]
+ xknx: XKNX = hass.data[DOMAIN].xknx
+
+ async_add_entities(
+ KNXSelect(xknx, entity_config) for entity_config in platform_config
+ )
+
+
+def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue:
+ """Return a KNX RawValue to be used within XKNX."""
+ return RawValue(
+ xknx,
+ name=config[CONF_NAME],
+ payload_length=config[SelectSchema.CONF_PAYLOAD_LENGTH],
+ group_address=config[KNX_ADDRESS],
+ group_address_state=config.get(CONF_STATE_ADDRESS),
+ respond_to_read=config[CONF_RESPOND_TO_READ],
+ sync_state=config[CONF_SYNC_STATE],
+ )
+
+
+class KNXSelect(KnxEntity, SelectEntity, RestoreEntity):
+ """Representation of a KNX select."""
+
+ _device: RawValue
+
+ def __init__(self, xknx: XKNX, config: ConfigType) -> None:
+ """Initialize a KNX select."""
+ super().__init__(_create_raw_value(xknx, config))
+ self._option_payloads: dict[str, int] = {
+ option[SelectSchema.CONF_OPTION]: option[SelectSchema.CONF_PAYLOAD]
+ for option in config[SelectSchema.CONF_OPTIONS]
+ }
+ self._attr_options = list(self._option_payloads)
+ self._attr_current_option = None
+ self._attr_unique_id = str(self._device.remote_value.group_address)
+
+ async def async_added_to_hass(self) -> None:
+ """Restore last state."""
+ await super().async_added_to_hass()
+ if not self._device.remote_value.readable and (
+ last_state := await self.async_get_last_state()
+ ):
+ if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
+ await self._device.remote_value.update_value(
+ self._option_payloads.get(last_state.state)
+ )
+
+ async def after_update_callback(self, device: XknxDevice) -> None:
+ """Call after device was updated."""
+ self._attr_current_option = self.option_from_payload(
+ self._device.remote_value.value
+ )
+ await super().after_update_callback(device)
+
+ def option_from_payload(self, payload: int | None) -> str | None:
+ """Return the option a given payload is assigned to."""
+ try:
+ return next(
+ key for key, value in self._option_payloads.items() if value == payload
+ )
+ except StopIteration:
+ return None
+
+ async def async_select_option(self, option: str) -> None:
+ """Change the selected option."""
+ payload = self._option_payloads.get(option)
+ if payload is None:
+ raise ValueError(f"Invalid option for {self.entity_id}: {option}")
+ await self._device.set(payload)
diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py
index 21586faf58c..e095b2aee47 100644
--- a/homeassistant/components/knx/sensor.py
+++ b/homeassistant/components/knx/sensor.py
@@ -27,15 +27,12 @@ async def async_setup_platform(
"""Set up sensor(s) for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
-
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx
- entities = []
- for entity_config in platform_config:
- entities.append(KNXSensor(xknx, entity_config))
-
- async_add_entities(entities)
+ async_add_entities(
+ KNXSensor(xknx, entity_config) for entity_config in platform_config
+ )
def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor:
@@ -53,30 +50,25 @@ def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor:
class KNXSensor(KnxEntity, SensorEntity):
"""Representation of a KNX sensor."""
+ _device: XknxSensor
+
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize of a KNX sensor."""
- self._device: XknxSensor
super().__init__(_create_sensor(xknx, config))
- self._unique_id = f"{self._device.sensor_value.group_address_state}"
+ self._attr_device_class = (
+ self._device.ha_device_class()
+ if self._device.ha_device_class() in DEVICE_CLASSES
+ else None
+ )
+ self._attr_force_update = self._device.always_callback
+ self._attr_unique_id = str(self._device.sensor_value.group_address_state)
+ self._attr_unit_of_measurement = self._device.unit_of_measurement()
@property
def state(self) -> StateType:
"""Return the state of the sensor."""
return self._device.resolve_state()
- @property
- def unit_of_measurement(self) -> str | None:
- """Return the unit this state is expressed in."""
- return self._device.unit_of_measurement()
-
- @property
- def device_class(self) -> str | None:
- """Return the device class of the sensor."""
- device_class = self._device.ha_device_class()
- if device_class in DEVICE_CLASSES:
- return device_class
- return None
-
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return device specific state attributes."""
@@ -88,13 +80,3 @@ class KNXSensor(KnxEntity, SensorEntity):
dt.as_utc(self._device.last_telegram.timestamp)
)
return attr
-
- @property
- def force_update(self) -> bool:
- """
- Return True if state updates should be forced.
-
- If True, a state change will be triggered anytime the state property is
- updated, not just when the value changes.
- """
- return self._device.always_callback
diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py
index c6fbb32b15d..cca0ffde853 100644
--- a/homeassistant/components/knx/switch.py
+++ b/homeassistant/components/knx/switch.py
@@ -7,12 +7,13 @@ from xknx import XKNX
from xknx.devices import Switch as XknxSwitch
from homeassistant.components.switch import SwitchEntity
-from homeassistant.const import CONF_NAME
+from homeassistant.const import CONF_NAME, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
-from .const import DOMAIN, KNX_ADDRESS
+from .const import CONF_RESPOND_TO_READ, DOMAIN, KNX_ADDRESS
from .knx_entity import KnxEntity
from .schema import SwitchSchema
@@ -26,33 +27,41 @@ async def async_setup_platform(
"""Set up switch(es) for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
-
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx
- entities = []
- for entity_config in platform_config:
- entities.append(KNXSwitch(xknx, entity_config))
-
- async_add_entities(entities)
+ async_add_entities(
+ KNXSwitch(xknx, entity_config) for entity_config in platform_config
+ )
-class KNXSwitch(KnxEntity, SwitchEntity):
+class KNXSwitch(KnxEntity, SwitchEntity, RestoreEntity):
"""Representation of a KNX switch."""
+ _device: XknxSwitch
+
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize of KNX switch."""
- self._device: XknxSwitch
super().__init__(
device=XknxSwitch(
xknx,
name=config[CONF_NAME],
group_address=config[KNX_ADDRESS],
group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS),
+ respond_to_read=config[CONF_RESPOND_TO_READ],
invert=config[SwitchSchema.CONF_INVERT],
)
)
- self._unique_id = f"{self._device.switch.group_address}"
+ self._attr_unique_id = str(self._device.switch.group_address)
+
+ async def async_added_to_hass(self) -> None:
+ """Restore last state."""
+ await super().async_added_to_hass()
+ if not self._device.switch.readable and (
+ last_state := await self.async_get_last_state()
+ ):
+ if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
+ self._device.switch.value = last_state.state == STATE_ON
@property
def is_on(self) -> bool:
diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py
index b396142e387..4a55f81ff72 100644
--- a/homeassistant/components/knx/weather.py
+++ b/homeassistant/components/knx/weather.py
@@ -24,15 +24,12 @@ async def async_setup_platform(
"""Set up weather entities for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
-
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx
- entities = []
- for entity_config in platform_config:
- entities.append(KNXWeather(xknx, entity_config))
-
- async_add_entities(entities)
+ async_add_entities(
+ KNXWeather(xknx, entity_config) for entity_config in platform_config
+ )
def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather:
@@ -74,22 +71,19 @@ def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather:
class KNXWeather(KnxEntity, WeatherEntity):
"""Representation of a KNX weather device."""
+ _device: XknxWeather
+ _attr_temperature_unit = TEMP_CELSIUS
+
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize of a KNX sensor."""
- self._device: XknxWeather
super().__init__(_create_weather(xknx, config))
- self._unique_id = f"{self._device._temperature.group_address_state}"
+ self._attr_unique_id = str(self._device._temperature.group_address_state)
@property
def temperature(self) -> float | None:
"""Return current temperature."""
return self._device.temperature
- @property
- def temperature_unit(self) -> str:
- """Return temperature unit."""
- return TEMP_CELSIUS
-
@property
def pressure(self) -> float | None:
"""Return current air pressure."""
diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py
index fe318b103d1..0b2b1b8047b 100644
--- a/homeassistant/components/kodi/__init__.py
+++ b/homeassistant/components/kodi/__init__.py
@@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["media_player"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Kodi from a config entry."""
conn = get_kodi_connection(
entry.data[CONF_HOST],
diff --git a/homeassistant/components/kodi/device_trigger.py b/homeassistant/components/kodi/device_trigger.py
index b8653290c0d..584e465b3a6 100644
--- a/homeassistant/components/kodi/device_trigger.py
+++ b/homeassistant/components/kodi/device_trigger.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_DEVICE_ID,
@@ -21,7 +21,7 @@ from .const import DOMAIN, EVENT_TURN_OFF, EVENT_TURN_ON
TRIGGER_TYPES = {"turn_on", "turn_off"}
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
@@ -67,7 +67,7 @@ def _attach_trigger(
event_type,
automation_info: dict,
):
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
job = HassJob(action)
@callback
@@ -75,7 +75,7 @@ def _attach_trigger(
if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]:
hass.async_run_hass_job(
job,
- {"trigger": {**config, "description": event_type, "id": trigger_id}},
+ {"trigger": {**trigger_data, **config, "description": event_type}},
event.context,
)
diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json
index 5f2badfd78d..80d47751006 100644
--- a/homeassistant/components/kodi/translations/de.json
+++ b/homeassistant/components/kodi/translations/de.json
@@ -12,7 +12,7 @@
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
- "flow_title": "Kodi: {name}",
+ "flow_title": "{name}",
"step": {
"credentials": {
"data": {
@@ -22,7 +22,8 @@
"description": "Bitte gib deinen Kodi-Benutzernamen und Passwort ein. Diese findest du unter System/Einstellungen/Netzwerk/Dienste."
},
"discovery_confirm": {
- "description": "M\u00f6chtest du Kodi (` {name} `) zu Home Assistant hinzuf\u00fcgen?"
+ "description": "M\u00f6chtest du Kodi (` {name} `) zu Home Assistant hinzuf\u00fcgen?",
+ "title": "Gefundene Kodi-Installation"
},
"user": {
"data": {
@@ -35,8 +36,15 @@
"ws_port": {
"data": {
"ws_port": "Port"
- }
+ },
+ "description": "Der WebSocket-Port (in Kodi manchmal TCP-Port genannt). Um eine Verbindung \u00fcber WebSocket herzustellen, m\u00fcssen Sie unter System/Einstellungen/Netzwerk/Dienste \"Programme ... zur Steuerung von Kodi zulassen\" aktivieren. Wenn WebSocket nicht aktiviert ist, entfernen Sie den Port und lassen ihn leer."
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_off": "{entity_name} wurde zum Ausschalten aufgefordert",
+ "turn_on": "{entity_name} wurde zum Einschalten aufgefordert"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/he.json b/homeassistant/components/kodi/translations/he.json
new file mode 100644
index 00000000000..07d8838f200
--- /dev/null
+++ b/homeassistant/components/kodi/translations/he.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\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",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_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}",
+ "step": {
+ "credentials": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ },
+ "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05d1-Kodi. \u05e0\u05d9\u05ea\u05df \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d5\u05ea\u05dd \u05d1\u05de\u05e2\u05e8\u05db\u05ea/\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea/\u05e8\u05e9\u05ea/\u05e9\u05d9\u05e8\u05d5\u05ea\u05d9\u05dd."
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
+ }
+ },
+ "ws_port": {
+ "data": {
+ "ws_port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/hu.json b/homeassistant/components/kodi/translations/hu.json
index 64dbfac0c8b..48ea9d954bd 100644
--- a/homeassistant/components/kodi/translations/hu.json
+++ b/homeassistant/components/kodi/translations/hu.json
@@ -11,7 +11,7 @@
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "Kodi: {name}",
+ "flow_title": "{name}",
"step": {
"credentials": {
"data": {
diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py
index 857521b9fad..4b5890532d1 100644
--- a/homeassistant/components/konnected/__init__.py
+++ b/homeassistant/components/konnected/__init__.py
@@ -250,7 +250,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up panel from a config entry."""
client = AlarmPanel(hass, entry)
# creates a panel data store in hass.data[DOMAIN][CONF_DEVICES]
diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json
index 7938f1a68bd..98862e85a8b 100644
--- a/homeassistant/components/konnected/translations/de.json
+++ b/homeassistant/components/konnected/translations/de.json
@@ -7,7 +7,7 @@
"unknown": "Unerwarteter Fehler"
},
"error": {
- "cannot_connect": "Es konnte keine Verbindung zu einem Konnected-Panel unter {host}:{port} hergestellt werden."
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"confirm": {
@@ -87,6 +87,7 @@
"data": {
"api_host": "API-Host-URL \u00fcberschreiben (optional)",
"blink": "LED Panel blinkt beim senden von Status\u00e4nderungen",
+ "discovery": "Reagieren auf Suchanfragen in Ihrem Netzwerk",
"override_api_host": "\u00dcberschreiben Sie die Standard-Host-Panel-URL der Home Assistant-API"
},
"description": "Bitte w\u00e4hlen Sie das gew\u00fcnschte Verhalten f\u00fcr Ihr Panel",
diff --git a/homeassistant/components/konnected/translations/he.json b/homeassistant/components/konnected/translations/he.json
new file mode 100644
index 00000000000..c31f537e698
--- /dev/null
+++ b/homeassistant/components/konnected/translations/he.json
@@ -0,0 +1,31 @@
+{
+ "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"
+ },
+ "step": {
+ "confirm": {
+ "description": "\u05d3\u05d2\u05dd: {model}\n\u05de\u05d6\u05d4\u05d4: {id}\n\u05de\u05d0\u05e8\u05d7: {host}\n\u05e4\u05ea\u05d7\u05d4: {port}\n\n\u05d1\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea\u05da \u05dc\u05e7\u05d1\u05d5\u05e2 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05e4\u05e2\u05dc\u05d4 \u05e9\u05dc \u05e4\u05ea\u05d9\u05d7\u05d4 \u05d5\u05e1\u05d2\u05d9\u05e8\u05d4 \u05d5\u05e9\u05dc \u05d4\u05d7\u05dc\u05d5\u05e0\u05d9\u05ea \u05d1\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05dc\u05d5\u05d7 \u05d4\u05d0\u05d6\u05e2\u05e7\u05d4 \u05e9\u05dc Konnected."
+ },
+ "user": {
+ "data": {
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ },
+ "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05de\u05d0\u05e8\u05d7 \u05e2\u05d1\u05d5\u05e8 \u05dc\u05d5\u05d7 Konnected \u05e9\u05dc\u05da."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "options_io": {
+ "description": "\u05d4\u05ea\u05d2\u05dc\u05d4 {model} \u05d1-{host} . \u05d1\u05d7\u05e8 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d1\u05e1\u05d9\u05e1 \u05e9\u05dc \u05db\u05dc \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05dc\u05de\u05d8\u05d4 - \u05d1\u05d4\u05ea\u05d0\u05dd \u05dc\u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d6\u05d4 \u05e2\u05e9\u05d5\u05d9 \u05dc\u05d0\u05e4\u05e9\u05e8 \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d1\u05d9\u05e0\u05d0\u05e8\u05d9\u05d9\u05dd (\u05de\u05d2\u05e2\u05d9\u05dd \u05e4\u05ea\u05d5\u05d7\u05d9\u05dd/\u05e1\u05d2\u05d5\u05e8\u05d9\u05dd), \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d3\u05d9\u05d2\u05d9\u05d8\u05dc\u05d9\u05d9\u05dd (dht \u05d5-ds18b20), \u05d0\u05d5 \u05d9\u05e6\u05d9\u05d0\u05d5\u05ea \u05e0\u05d9\u05ea\u05e0\u05d5\u05ea \u05dc\u05d4\u05d7\u05dc\u05e4\u05d4. \u05ea\u05d5\u05db\u05dc \u05dc\u05e7\u05d1\u05d5\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05e4\u05d5\u05e8\u05d8\u05d5\u05ea \u05d1\u05e9\u05dc\u05d1\u05d9\u05dd \u05d4\u05d1\u05d0\u05d9\u05dd."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json
index da88fb0ac4d..6b41217dca4 100644
--- a/homeassistant/components/konnected/translations/it.json
+++ b/homeassistant/components/konnected/translations/it.json
@@ -32,7 +32,9 @@
"not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto"
},
"error": {
- "bad_host": "URL host API di sostituzione non valido"
+ "bad_host": "URL host API di sostituzione non valido",
+ "one": "Pi\u00f9",
+ "other": "Altri"
},
"step": {
"options_binary": {
diff --git a/homeassistant/components/kostal_plenticore/translations/de.json b/homeassistant/components/kostal_plenticore/translations/de.json
index 095487fff3f..dfd568f937c 100644
--- a/homeassistant/components/kostal_plenticore/translations/de.json
+++ b/homeassistant/components/kostal_plenticore/translations/de.json
@@ -16,5 +16,6 @@
}
}
}
- }
+ },
+ "title": "Kostal Plenticore Solar-Wechselrichter"
}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/he.json b/homeassistant/components/kostal_plenticore/translations/he.json
new file mode 100644
index 00000000000..56476cccb7c
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/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"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py
index d52e0712a0b..76a4976f163 100644
--- a/homeassistant/components/kraken/__init__.py
+++ b/homeassistant/components/kraken/__init__.py
@@ -25,20 +25,20 @@ from .const import (
)
from .utils import get_tradable_asset_pairs
+CALL_RATE_LIMIT_SLEEP = 1
+
PLATFORMS = ["sensor"]
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up kraken from a config entry."""
- kraken_data = KrakenData(hass, config_entry)
+ kraken_data = KrakenData(hass, entry)
await kraken_data.async_setup()
hass.data[DOMAIN] = kraken_data
- config_entry.async_on_unload(
- config_entry.add_update_listener(async_options_updated)
- )
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ entry.async_on_unload(entry.add_update_listener(async_options_updated))
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
@@ -127,7 +127,8 @@ class KrakenData:
self._config_entry, options=options
)
await self._async_refresh_tradable_asset_pairs()
- await asyncio.sleep(1) # Wait 1 second to avoid triggering the CallRateLimiter
+ # Wait 1 second to avoid triggering the CallRateLimiter
+ await asyncio.sleep(CALL_RATE_LIMIT_SLEEP)
self.coordinator = DataUpdateCoordinator(
self._hass,
_LOGGER,
diff --git a/homeassistant/components/kraken/config_flow.py b/homeassistant/components/kraken/config_flow.py
index a34bf78557e..68443705767 100644
--- a/homeassistant/components/kraken/config_flow.py
+++ b/homeassistant/components/kraken/config_flow.py
@@ -24,7 +24,6 @@ class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for kraken."""
VERSION = 1
- CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
@staticmethod
@callback
@@ -38,7 +37,7 @@ class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
- if DOMAIN in self.hass.data:
+ if self._async_current_entries():
return self.async_abort(reason="already_configured")
if user_input is not None:
return self.async_create_entry(title=DOMAIN, data=user_input)
diff --git a/homeassistant/components/kraken/strings.json b/homeassistant/components/kraken/strings.json
index e94f2129a48..10257793de0 100644
--- a/homeassistant/components/kraken/strings.json
+++ b/homeassistant/components/kraken/strings.json
@@ -3,10 +3,8 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]"
},
- "error": {},
"step": {
"user": {
- "data": {},
"description": "[%key:common::config_flow::description::confirm_setup%]"
}
}
diff --git a/homeassistant/components/kraken/translations/de.json b/homeassistant/components/kraken/translations/de.json
index 9a5d52f939e..d0a845edfd4 100644
--- a/homeassistant/components/kraken/translations/de.json
+++ b/homeassistant/components/kraken/translations/de.json
@@ -13,6 +13,7 @@
"step": {
"init": {
"data": {
+ "scan_interval": "Update-Intervall",
"tracked_asset_pairs": "Verfolgte Asset-Paare"
}
}
diff --git a/homeassistant/components/kraken/translations/es.json b/homeassistant/components/kraken/translations/es.json
new file mode 100644
index 00000000000..afcf3f92d45
--- /dev/null
+++ b/homeassistant/components/kraken/translations/es.json
@@ -0,0 +1,12 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Intervalo de actualizaci\u00f3n",
+ "tracked_asset_pairs": "Pares de activos rastreados"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kraken/translations/et.json b/homeassistant/components/kraken/translations/et.json
index 74693aa9525..d0603f324bf 100644
--- a/homeassistant/components/kraken/translations/et.json
+++ b/homeassistant/components/kraken/translations/et.json
@@ -5,7 +5,7 @@
},
"step": {
"user": {
- "description": "Kinnita s\u00e4tted"
+ "description": "Kas soovid alustada seadistamist?"
}
}
},
diff --git a/homeassistant/components/kraken/translations/he.json b/homeassistant/components/kraken/translations/he.json
new file mode 100644
index 00000000000..4676729e600
--- /dev/null
+++ b/homeassistant/components/kraken/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\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?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kraken/translations/hu.json b/homeassistant/components/kraken/translations/hu.json
new file mode 100644
index 00000000000..4901da74d90
--- /dev/null
+++ b/homeassistant/components/kraken/translations/hu.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "step": {
+ "user": {
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kraken/translations/it.json b/homeassistant/components/kraken/translations/it.json
index a436844fb75..4b646be7a8d 100644
--- a/homeassistant/components/kraken/translations/it.json
+++ b/homeassistant/components/kraken/translations/it.json
@@ -3,8 +3,16 @@
"abort": {
"already_configured": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
+ "error": {
+ "one": "Pi\u00f9",
+ "other": "Altri"
+ },
"step": {
"user": {
+ "data": {
+ "one": "Pi\u00f9",
+ "other": "Altri"
+ },
"description": "Vuoi iniziare la configurazione?"
}
}
diff --git a/homeassistant/components/kraken/translations/pl.json b/homeassistant/components/kraken/translations/pl.json
new file mode 100644
index 00000000000..288b3b1d2b2
--- /dev/null
+++ b/homeassistant/components/kraken/translations/pl.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
+ },
+ "error": {
+ "few": "kilka",
+ "many": "wiele",
+ "one": "jeden",
+ "other": "inne"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "few": "kilka",
+ "many": "wiele",
+ "one": "jeden",
+ "other": "inne"
+ },
+ "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji",
+ "tracked_asset_pairs": "\u015aledzone pary walut"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py
index 6409d435bf3..03819c360d6 100644
--- a/homeassistant/components/kulersky/__init__.py
+++ b/homeassistant/components/kulersky/__init__.py
@@ -8,7 +8,7 @@ from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN
PLATFORMS = ["light"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Kuler Sky from a config entry."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py
index 48f27e91c79..fd907235b45 100644
--- a/homeassistant/components/kulersky/light.py
+++ b/homeassistant/components/kulersky/light.py
@@ -8,11 +8,8 @@ import pykulersky
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
- ATTR_HS_COLOR,
- ATTR_WHITE_VALUE,
- SUPPORT_BRIGHTNESS,
- SUPPORT_COLOR,
- SUPPORT_WHITE_VALUE,
+ ATTR_RGBW_COLOR,
+ COLOR_MODE_RGBW,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
@@ -20,14 +17,11 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
-import homeassistant.util.color as color_util
from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN
_LOGGER = logging.getLogger(__name__)
-SUPPORT_KULERSKY = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE
-
DISCOVERY_INTERVAL = timedelta(seconds=60)
@@ -71,10 +65,9 @@ class KulerskyLight(LightEntity):
def __init__(self, light: pykulersky.Light) -> None:
"""Initialize a Kuler Sky light."""
self._light = light
- self._hs_color = None
- self._brightness = None
- self._white_value = None
self._available = None
+ self._attr_supported_color_modes = {COLOR_MODE_RGBW}
+ self._attr_color_mode = COLOR_MODE_RGBW
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
@@ -112,30 +105,10 @@ class KulerskyLight(LightEntity):
"manufacturer": "Brightech",
}
- @property
- def supported_features(self):
- """Flag supported features."""
- return SUPPORT_KULERSKY
-
- @property
- def brightness(self):
- """Return the brightness of the light."""
- return self._brightness
-
- @property
- def hs_color(self):
- """Return the hs color."""
- return self._hs_color
-
- @property
- def white_value(self):
- """Return the white value of this light between 0..255."""
- return self._white_value
-
@property
def is_on(self):
"""Return true if light is on."""
- return self._brightness > 0 or self._white_value > 0
+ return self.brightness > 0
@property
def available(self) -> bool:
@@ -144,24 +117,21 @@ class KulerskyLight(LightEntity):
async def async_turn_on(self, **kwargs):
"""Instruct the light to turn on."""
- default_hs = (0, 0) if self._hs_color is None else self._hs_color
- hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs)
+ default_rgbw = (255,) * 4 if self.rgbw_color is None else self.rgbw_color
+ rgbw = kwargs.get(ATTR_RGBW_COLOR, default_rgbw)
- default_brightness = 0 if self._brightness is None else self._brightness
+ default_brightness = 0 if self.brightness is None else self.brightness
brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness)
- default_white_value = 255 if self._white_value is None else self._white_value
- white_value = kwargs.get(ATTR_WHITE_VALUE, default_white_value)
-
- if brightness == 0 and white_value == 0 and not kwargs:
+ if brightness == 0 and not kwargs:
# If the light would be off, and no additional parameters were
# passed, just turn the light on full brightness.
brightness = 255
- white_value = 255
+ rgbw = (255,) * 4
- rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100)
+ rgbw_scaled = [round(x * brightness / 255) for x in rgbw]
- await self._light.set_color(*rgb, white_value)
+ await self._light.set_color(*rgbw_scaled)
async def async_turn_off(self, **kwargs):
"""Instruct the light to turn off."""
@@ -173,7 +143,7 @@ class KulerskyLight(LightEntity):
if not self._available:
await self._light.connect()
# pylint: disable=invalid-name
- r, g, b, w = await self._light.get_color()
+ rgbw = await self._light.get_color()
except pykulersky.PykulerskyException as exc:
if self._available:
_LOGGER.warning("Unable to connect to %s: %s", self._light.address, exc)
@@ -183,7 +153,10 @@ class KulerskyLight(LightEntity):
_LOGGER.info("Reconnected to %s", self._light.address)
self._available = True
- hsv = color_util.color_RGB_to_hsv(r, g, b)
- self._hs_color = hsv[:2]
- self._brightness = int(round((hsv[2] / 100) * 255))
- self._white_value = w
+ brightness = max(rgbw)
+ if not brightness:
+ rgbw_normalized = [0, 0, 0, 0]
+ else:
+ rgbw_normalized = [round(x * 255 / brightness) for x in rgbw]
+ self._attr_brightness = brightness
+ self._attr_rgbw_color = tuple(rgbw_normalized)
diff --git a/homeassistant/components/kulersky/translations/he.json b/homeassistant/components/kulersky/translations/he.json
new file mode 100644
index 00000000000..d3d68dccc93
--- /dev/null
+++ b/homeassistant/components/kulersky/translations/he.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\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": {
+ "confirm": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py
index 32090797f11..7c5557757ef 100644
--- a/homeassistant/components/lacrosse/sensor.py
+++ b/homeassistant/components/lacrosse/sensor.py
@@ -68,7 +68,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the LaCrosse sensors."""
-
usb_device = config.get(CONF_DEVICE)
baud = int(config.get(CONF_BAUD))
expire_after = config.get(CONF_EXPIRE_AFTER)
@@ -127,28 +126,22 @@ class LaCrosseSensor(SensorEntity):
ENTITY_ID_FORMAT, device_id, hass=hass
)
self._config = config
- self._name = name
self._value = None
self._expire_after = expire_after
self._expiration_trigger = None
+ self._attr_name = name
lacrosse.register_callback(
int(self._config["id"]), self._callback_lacrosse, None
)
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
@property
def extra_state_attributes(self):
"""Return the state attributes."""
- attributes = {
+ return {
"low_battery": self._low_battery,
"new_battery": self._new_battery,
}
- return attributes
def _callback_lacrosse(self, lacrosse_sensor, user_data):
"""Handle a function that is called from pylacrosse with new values."""
@@ -181,10 +174,7 @@ class LaCrosseSensor(SensorEntity):
class LaCrosseTemperature(LaCrosseSensor):
"""Implementation of a Lacrosse temperature sensor."""
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return TEMP_CELSIUS
+ _attr_unit_of_measurement = TEMP_CELSIUS
@property
def state(self):
@@ -195,21 +185,14 @@ class LaCrosseTemperature(LaCrosseSensor):
class LaCrosseHumidity(LaCrosseSensor):
"""Implementation of a Lacrosse humidity sensor."""
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return PERCENTAGE
+ _attr_unit_of_measurement = PERCENTAGE
+ _attr_icon = "mdi:water-percent"
@property
def state(self):
"""Return the state of the sensor."""
return self._humidity
- @property
- def icon(self):
- """Icon to use in the frontend."""
- return "mdi:water-percent"
-
class LaCrosseBattery(LaCrosseSensor):
"""Implementation of a Lacrosse battery sensor."""
@@ -218,23 +201,19 @@ class LaCrosseBattery(LaCrosseSensor):
def state(self):
"""Return the state of the sensor."""
if self._low_battery is None:
- state = None
- elif self._low_battery is True:
- state = "low"
- else:
- state = "ok"
- return state
+ return None
+ if self._low_battery is True:
+ return "low"
+ return "ok"
@property
def icon(self):
"""Icon to use in the frontend."""
if self._low_battery is None:
- icon = "mdi:battery-unknown"
- elif self._low_battery is True:
- icon = "mdi:battery-alert"
- else:
- icon = "mdi:battery"
- return icon
+ return "mdi:battery-unknown"
+ if self._low_battery is True:
+ return "mdi:battery-alert"
+ return "mdi:battery"
TYPE_CLASSES = {
diff --git a/homeassistant/components/life360/translations/he.json b/homeassistant/components/life360/translations/he.json
index 3007c0e968c..e6fa6cd1db9 100644
--- a/homeassistant/components/life360/translations/he.json
+++ b/homeassistant/components/life360/translations/he.json
@@ -1,9 +1,20 @@
{
"config": {
+ "abort": {
+ "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"
+ },
+ "error": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "invalid_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
"user": {
"data": {
- "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
}
diff --git a/homeassistant/components/lifx/translations/he.json b/homeassistant/components/lifx/translations/he.json
new file mode 100644
index 00000000000..380dbc5d7fc
--- /dev/null
+++ b/homeassistant/components/lifx/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py
index 27f3bbfc0c6..e92999f4d21 100644
--- a/homeassistant/components/light/__init__.py
+++ b/homeassistant/components/light/__init__.py
@@ -18,8 +18,8 @@ from homeassistant.const import (
SERVICE_TURN_ON,
STATE_ON,
)
-from homeassistant.core import HomeAssistant, callback
-import homeassistant.helpers.config_validation as cv
+from homeassistant.core import HomeAssistant, HomeAssistantError, callback
+from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
@@ -61,6 +61,7 @@ COLOR_MODE_XY = "xy"
COLOR_MODE_RGB = "rgb"
COLOR_MODE_RGBW = "rgbw"
COLOR_MODE_RGBWW = "rgbww"
+COLOR_MODE_WHITE = "white" # Must *NOT* be the only supported mode
VALID_COLOR_MODES = {
COLOR_MODE_ONOFF,
@@ -71,6 +72,7 @@ VALID_COLOR_MODES = {
COLOR_MODE_RGB,
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
+ COLOR_MODE_WHITE,
}
COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF}
COLOR_MODES_COLOR = {
@@ -90,6 +92,7 @@ def valid_supported_color_modes(color_modes: Iterable[str]) -> set[str]:
or COLOR_MODE_UNKNOWN in color_modes
or (COLOR_MODE_BRIGHTNESS in color_modes and len(color_modes) > 1)
or (COLOR_MODE_ONOFF in color_modes and len(color_modes) > 1)
+ or (COLOR_MODE_WHITE in color_modes and not color_supported(color_modes))
):
raise vol.Error(f"Invalid supported_color_modes {sorted(color_modes)}")
return color_modes
@@ -116,6 +119,26 @@ def color_temp_supported(color_modes: Iterable[str] | None) -> bool:
return COLOR_MODE_COLOR_TEMP in color_modes
+def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set | None:
+ """Get supported color modes for a light entity.
+
+ First try the statemachine, then entity registry.
+ This is the equivalent of entity helper get_supported_features.
+ """
+ state = hass.states.get(entity_id)
+ if state:
+ return state.attributes.get(ATTR_SUPPORTED_COLOR_MODES)
+
+ entity_registry = er.async_get(hass)
+ entry = entity_registry.async_get(entity_id)
+ if not entry:
+ raise HomeAssistantError(f"Unknown entity {entity_id}")
+ if not entry.capabilities:
+ return None
+
+ return entry.capabilities.get(ATTR_SUPPORTED_COLOR_MODES)
+
+
# Float that represents transition time in seconds to make change.
ATTR_TRANSITION = "transition"
@@ -131,6 +154,7 @@ ATTR_MIN_MIREDS = "min_mireds"
ATTR_MAX_MIREDS = "max_mireds"
ATTR_COLOR_NAME = "color_name"
ATTR_WHITE_VALUE = "white_value"
+ATTR_WHITE = "white"
# Brightness of the light, 0..255 or percentage
ATTR_BRIGHTNESS = "brightness"
@@ -175,6 +199,19 @@ LIGHT_TURN_ON_SCHEMA = {
vol.Exclusive(ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP,
vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT,
vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
+ vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All(
+ vol.Coerce(int), vol.Range(min=1)
+ ),
+ vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int,
+ vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All(
+ vol.ExactSequence(
+ (
+ vol.All(vol.Coerce(float), vol.Range(min=0, max=360)),
+ vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
+ )
+ ),
+ vol.Coerce(tuple),
+ ),
vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All(
vol.ExactSequence((cv.byte,) * 3), vol.Coerce(tuple)
),
@@ -187,19 +224,7 @@ LIGHT_TURN_ON_SCHEMA = {
vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All(
vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple)
),
- vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All(
- vol.ExactSequence(
- (
- vol.All(vol.Coerce(float), vol.Range(min=0, max=360)),
- vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
- )
- ),
- vol.Coerce(tuple),
- ),
- vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All(
- vol.Coerce(int), vol.Range(min=1)
- ),
- vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int,
+ vol.Exclusive(ATTR_WHITE, COLOR_GROUP): VALID_BRIGHTNESS,
ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
ATTR_FLASH: VALID_FLASH,
ATTR_EFFECT: cv.string,
@@ -248,7 +273,7 @@ def preprocess_turn_on_alternatives(hass, params):
def filter_turn_off_params(light, params):
- """Filter out params not used in turn off."""
+ """Filter out params not used in turn off or not supported by the light."""
supported_features = light.supported_features
if not supported_features & SUPPORT_FLASH:
@@ -260,7 +285,7 @@ def filter_turn_off_params(light, params):
def filter_turn_on_params(light, params):
- """Filter out params not used in turn off."""
+ """Filter out params not supported by the light."""
supported_features = light.supported_features
if not supported_features & SUPPORT_EFFECT:
@@ -287,6 +312,8 @@ def filter_turn_on_params(light, params):
params.pop(ATTR_RGBW_COLOR, None)
if COLOR_MODE_RGBWW not in supported_color_modes:
params.pop(ATTR_RGBWW_COLOR, None)
+ if COLOR_MODE_WHITE not in supported_color_modes:
+ params.pop(ATTR_WHITE, None)
if COLOR_MODE_XY not in supported_color_modes:
params.pop(ATTR_XY_COLOR, None)
@@ -343,13 +370,13 @@ async def async_setup(hass, config): # noqa: C901
):
profiles.apply_default(light.entity_id, light.is_on, params)
+ legacy_supported_color_modes = (
+ light._light_internal_supported_color_modes # pylint: disable=protected-access
+ )
supported_color_modes = light.supported_color_modes
# Backwards compatibility: if an RGBWW color is specified, convert to RGB + W
# for legacy lights
if ATTR_RGBW_COLOR in params:
- legacy_supported_color_modes = (
- light._light_internal_supported_color_modes # pylint: disable=protected-access
- )
if (
COLOR_MODE_RGBW in legacy_supported_color_modes
and not supported_color_modes
@@ -358,6 +385,16 @@ async def async_setup(hass, config): # noqa: C901
params[ATTR_RGB_COLOR] = rgbw_color[0:3]
params[ATTR_WHITE_VALUE] = rgbw_color[3]
+ # If a color temperature is specified, emulate it if not supported by the light
+ if (
+ ATTR_COLOR_TEMP in params
+ and COLOR_MODE_COLOR_TEMP not in legacy_supported_color_modes
+ ):
+ color_temp = params.pop(ATTR_COLOR_TEMP)
+ if color_supported(legacy_supported_color_modes):
+ temp_k = color_util.color_temperature_mired_to_kelvin(color_temp)
+ params[ATTR_HS_COLOR] = color_util.color_temperature_to_hs(temp_k)
+
# If a color is specified, convert to the color space supported by the light
# Backwards compatibility: Fall back to hs color if light.supported_color_modes
# is not implemented
@@ -407,11 +444,15 @@ async def async_setup(hass, config): # noqa: C901
*rgb_color, light.min_mireds, light.max_mireds
)
+ # If both white and brightness are specified, override white
+ if ATTR_WHITE in params and COLOR_MODE_WHITE in supported_color_modes:
+ params[ATTR_WHITE] = params.pop(ATTR_BRIGHTNESS, params[ATTR_WHITE])
+
# Remove deprecated white value if the light supports color mode
if supported_color_modes:
params.pop(ATTR_WHITE_VALUE, None)
- if params.get(ATTR_BRIGHTNESS) == 0:
+ if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0:
await async_handle_light_off_service(light, call)
else:
await light.async_turn_on(**filter_turn_on_params(light, params))
@@ -606,8 +647,8 @@ class LightEntity(ToggleEntity):
_attr_effect_list: list[str] | None = None
_attr_effect: str | None = None
_attr_hs_color: tuple[float, float] | None = None
- _attr_max_mired: int = 500
- _attr_min_mired: int = 153
+ _attr_max_mireds: int = 500
+ _attr_min_mireds: int = 153
_attr_rgb_color: tuple[int, int, int] | None = None
_attr_rgbw_color: tuple[int, int, int, int] | None = None
_attr_rgbww_color: tuple[int, int, int, int, int] | None = None
@@ -707,14 +748,14 @@ class LightEntity(ToggleEntity):
"""Return the coldest color_temp that this light supports."""
# Default to the Philips Hue value that HA has always assumed
# https://developers.meethue.com/documentation/core-concepts
- return self._attr_min_mired
+ return self._attr_min_mireds
@property
def max_mireds(self) -> int:
"""Return the warmest color_temp that this light supports."""
# Default to the Philips Hue value that HA has always assumed
# https://developers.meethue.com/documentation/core-concepts
- return self._attr_max_mired
+ return self._attr_max_mireds
@property
def white_value(self) -> int | None:
diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py
index 3de2218d7c7..2180bdd3094 100644
--- a/homeassistant/components/light/device_action.py
+++ b/homeassistant/components/light/device_action.py
@@ -11,7 +11,14 @@ from homeassistant.components.light import (
VALID_BRIGHTNESS_PCT,
VALID_FLASH,
)
-from homeassistant.const import ATTR_ENTITY_ID, CONF_DOMAIN, CONF_TYPE, SERVICE_TURN_ON
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ CONF_ENTITY_ID,
+ CONF_TYPE,
+ SERVICE_TURN_ON,
+)
from homeassistant.core import Context, HomeAssistant, HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity import get_supported_features
@@ -20,9 +27,9 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import (
ATTR_BRIGHTNESS_PCT,
ATTR_BRIGHTNESS_STEP_PCT,
- ATTR_SUPPORTED_COLOR_MODES,
DOMAIN,
brightness_supported,
+ get_supported_color_modes,
)
TYPE_BRIGHTNESS_INCREASE = "brightness_increase"
@@ -43,25 +50,6 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
)
-def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set | None:
- """Get supported color modes for a light entity.
-
- First try the statemachine, then entity registry.
- """
- state = hass.states.get(entity_id)
- if state:
- return state.attributes.get(ATTR_SUPPORTED_COLOR_MODES)
-
- entity_registry = er.async_get(hass)
- entry = entity_registry.async_get(entity_id)
- if not entry:
- raise HomeAssistantError(f"Unknown entity {entity_id}")
- if not entry.capabilities:
- return None
-
- return entry.capabilities.get(ATTR_SUPPORTED_COLOR_MODES)
-
-
async def async_call_action_from_config(
hass: HomeAssistant,
config: ConfigType,
@@ -111,35 +99,22 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
supported_color_modes = get_supported_color_modes(hass, entry.entity_id)
supported_features = get_supported_features(hass, entry.entity_id)
+ base_action = {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
if brightness_supported(supported_color_modes):
actions.extend(
(
- {
- CONF_TYPE: TYPE_BRIGHTNESS_INCREASE,
- "device_id": device_id,
- "entity_id": entry.entity_id,
- "domain": DOMAIN,
- },
- {
- CONF_TYPE: TYPE_BRIGHTNESS_DECREASE,
- "device_id": device_id,
- "entity_id": entry.entity_id,
- "domain": DOMAIN,
- },
+ {**base_action, CONF_TYPE: TYPE_BRIGHTNESS_INCREASE},
+ {**base_action, CONF_TYPE: TYPE_BRIGHTNESS_DECREASE},
)
)
if supported_features & SUPPORT_FLASH:
- actions.extend(
- (
- {
- CONF_TYPE: TYPE_FLASH,
- "device_id": device_id,
- "entity_id": entry.entity_id,
- "domain": DOMAIN,
- },
- )
- )
+ actions.append({**base_action, CONF_TYPE: TYPE_FLASH})
return actions
diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py
index fa70670eee7..77e5742bbab 100644
--- a/homeassistant/components/light/reproduce_state.py
+++ b/homeassistant/components/light/reproduce_state.py
@@ -5,7 +5,7 @@ import asyncio
from collections.abc import Iterable
import logging
from types import MappingProxyType
-from typing import Any
+from typing import Any, cast
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -31,6 +31,7 @@ from . import (
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_TRANSITION,
+ ATTR_WHITE,
ATTR_WHITE_VALUE,
ATTR_XY_COLOR,
COLOR_MODE_COLOR_TEMP,
@@ -39,6 +40,7 @@ from . import (
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
COLOR_MODE_UNKNOWN,
+ COLOR_MODE_WHITE,
COLOR_MODE_XY,
DOMAIN,
)
@@ -70,12 +72,13 @@ COLOR_GROUP = [
]
COLOR_MODE_TO_ATTRIBUTE = {
- COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP,
- COLOR_MODE_HS: ATTR_HS_COLOR,
- COLOR_MODE_RGB: ATTR_RGB_COLOR,
- COLOR_MODE_RGBW: ATTR_RGBW_COLOR,
- COLOR_MODE_RGBWW: ATTR_RGBWW_COLOR,
- COLOR_MODE_XY: ATTR_XY_COLOR,
+ COLOR_MODE_COLOR_TEMP: (ATTR_COLOR_TEMP, ATTR_COLOR_TEMP),
+ COLOR_MODE_HS: (ATTR_HS_COLOR, ATTR_HS_COLOR),
+ COLOR_MODE_RGB: (ATTR_RGB_COLOR, ATTR_RGB_COLOR),
+ COLOR_MODE_RGBW: (ATTR_RGBW_COLOR, ATTR_RGBW_COLOR),
+ COLOR_MODE_RGBWW: (ATTR_RGBWW_COLOR, ATTR_RGBWW_COLOR),
+ COLOR_MODE_WHITE: (ATTR_WHITE, ATTR_BRIGHTNESS),
+ COLOR_MODE_XY: (ATTR_XY_COLOR, ATTR_XY_COLOR),
}
DEPRECATED_GROUP = [
@@ -93,6 +96,17 @@ DEPRECATION_WARNING = (
)
+def _color_mode_same(cur_state: State, state: State) -> bool:
+ """Test if color_mode is same."""
+ cur_color_mode = cur_state.attributes.get(ATTR_COLOR_MODE, COLOR_MODE_UNKNOWN)
+ saved_color_mode = state.attributes.get(ATTR_COLOR_MODE, COLOR_MODE_UNKNOWN)
+
+ # Guard for scenes etc. which where created before color modes were introduced
+ if saved_color_mode == COLOR_MODE_UNKNOWN:
+ return True
+ return cast(bool, cur_color_mode == saved_color_mode)
+
+
async def _async_reproduce_state(
hass: HomeAssistant,
state: State,
@@ -119,9 +133,13 @@ async def _async_reproduce_state(
_LOGGER.warning(DEPRECATION_WARNING, deprecated_attrs)
# Return if we are already at the right state.
- if cur_state.state == state.state and all(
- check_attr_equal(cur_state.attributes, state.attributes, attr)
- for attr in ATTR_GROUP + COLOR_GROUP
+ if (
+ cur_state.state == state.state
+ and _color_mode_same(cur_state, state)
+ and all(
+ check_attr_equal(cur_state.attributes, state.attributes, attr)
+ for attr in ATTR_GROUP + COLOR_GROUP
+ )
):
return
@@ -144,16 +162,17 @@ async def _async_reproduce_state(
# Remove deprecated white value if we got a valid color mode
service_data.pop(ATTR_WHITE_VALUE, None)
color_mode = state.attributes[ATTR_COLOR_MODE]
- if color_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode):
- if color_attr not in state.attributes:
+ if parameter_state := COLOR_MODE_TO_ATTRIBUTE.get(color_mode):
+ parameter, state_attr = parameter_state
+ if state_attr not in state.attributes:
_LOGGER.warning(
"Color mode %s specified but attribute %s missing for: %s",
color_mode,
- color_attr,
+ state_attr,
state.entity_id,
)
return
- service_data[color_attr] = state.attributes[color_attr]
+ service_data[parameter] = state.attributes[state_attr]
else:
# Fall back to Choosing the first color that is specified
for color_attr in COLOR_GROUP:
diff --git a/homeassistant/components/light/translations/he.json b/homeassistant/components/light/translations/he.json
index bb49ba266a7..a61237ba51e 100644
--- a/homeassistant/components/light/translations/he.json
+++ b/homeassistant/components/light/translations/he.json
@@ -1,8 +1,26 @@
{
+ "device_automation": {
+ "action_type": {
+ "brightness_decrease": "\u05d4\u05e4\u05d7\u05ea \u05d0\u05ea \u05d4\u05d1\u05d4\u05d9\u05e8\u05d5\u05ea \u05e9\u05dc {entity_name}",
+ "brightness_increase": "\u05d4\u05d2\u05d1\u05e8 \u05d0\u05ea \u05d4\u05d1\u05d4\u05d9\u05e8\u05d5\u05ea \u05e9\u05dc {entity_name}",
+ "flash": "\u05d4\u05d1\u05d4\u05d1 \u05d0\u05ea {entity_name}",
+ "toggle": "\u05d4\u05d7\u05dc\u05e3 \u05d0\u05ea {entity_name}",
+ "turn_off": "\u05db\u05d1\u05d4 \u05d0\u05ea {entity_name}",
+ "turn_on": "\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9",
+ "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4",
+ "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc"
+ }
+ },
"state": {
"_": {
"off": "\u05db\u05d1\u05d5\u05d9",
- "on": "\u05d3\u05dc\u05d5\u05e7"
+ "on": "\u05de\u05d5\u05e4\u05e2\u05dc"
}
},
"title": "\u05d0\u05d5\u05b9\u05e8"
diff --git a/homeassistant/components/lightwave/climate.py b/homeassistant/components/lightwave/climate.py
index 0518e91dda9..44b1e29ff34 100644
--- a/homeassistant/components/lightwave/climate.py
+++ b/homeassistant/components/lightwave/climate.py
@@ -42,6 +42,7 @@ class LightwaveTrv(ClimateEntity):
self._hvac_action = None
self._lwlink = lwlink
self._serial = serial
+ self._attr_unique_id = f"{serial}-trv"
# inhibit is used to prevent race condition on update. If non zero, skip next update cycle.
self._inhibit = 0
diff --git a/homeassistant/components/lightwave/sensor.py b/homeassistant/components/lightwave/sensor.py
index 1128078f8bc..b298b78c7f6 100644
--- a/homeassistant/components/lightwave/sensor.py
+++ b/homeassistant/components/lightwave/sensor.py
@@ -1,5 +1,5 @@
"""Support for LightwaveRF TRV - Associated Battery."""
-from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY, PERCENTAGE
from . import CONF_SERIAL, LIGHTWAVE_LINK
@@ -25,17 +25,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class LightwaveBattery(SensorEntity):
"""Lightwave TRV Battery."""
+ _attr_device_class = DEVICE_CLASS_BATTERY
+ _attr_unit_of_measurement = PERCENTAGE
+ _attr_state_class = STATE_CLASS_MEASUREMENT
+
def __init__(self, name, lwlink, serial):
"""Initialize the Lightwave Trv battery sensor."""
self._name = name
self._state = None
self._lwlink = lwlink
self._serial = serial
-
- @property
- def device_class(self):
- """Return the device class of the sensor."""
- return DEVICE_CLASS_BATTERY
+ self._attr_unique_id = f"{serial}-trv-battery"
@property
def name(self):
@@ -47,11 +47,6 @@ class LightwaveBattery(SensorEntity):
"""Return the state of the sensor."""
return self._state
- @property
- def unit_of_measurement(self):
- """Return the state of the sensor."""
- return PERCENTAGE
-
def update(self):
"""Communicate with a Lightwave RTF Proxy to get state."""
(dummy_temp, dummy_targ, battery, dummy_output) = self._lwlink.read_trv_status(
diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py
index 70a15eaf4e0..6769d72594b 100644
--- a/homeassistant/components/linode/binary_sensor.py
+++ b/homeassistant/components/linode/binary_sensor.py
@@ -50,6 +50,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class LinodeBinarySensor(BinarySensorEntity):
"""Representation of a Linode droplet sensor."""
+ _attr_device_class = DEVICE_CLASS_MOVING
+
def __init__(self, li, node_id):
"""Initialize a new Linode sensor."""
self._linode = li
@@ -69,11 +71,6 @@ class LinodeBinarySensor(BinarySensorEntity):
"""Return true if the binary sensor is on."""
return self._state
- @property
- def device_class(self):
- """Return the class of this sensor."""
- return DEVICE_CLASS_MOVING
-
@property
def extra_state_attributes(self):
"""Return the state attributes of the Linode Node."""
diff --git a/homeassistant/components/litejet/translations/he.json b/homeassistant/components/litejet/translations/he.json
index a06c89f1d2a..406e020905e 100644
--- a/homeassistant/components/litejet/translations/he.json
+++ b/homeassistant/components/litejet/translations/he.json
@@ -1,5 +1,8 @@
{
"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": {
"data": {
diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py
index 6800282766b..3a9930c5e70 100644
--- a/homeassistant/components/litejet/trigger.py
+++ b/homeassistant/components/litejet/trigger.py
@@ -15,7 +15,7 @@ CONF_NUMBER = "number"
CONF_HELD_MORE_THAN = "held_more_than"
CONF_HELD_LESS_THAN = "held_less_than"
-TRIGGER_SCHEMA = vol.Schema(
+TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "litejet",
vol.Required(CONF_NUMBER): cv.positive_int,
@@ -31,7 +31,7 @@ TRIGGER_SCHEMA = vol.Schema(
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for events based on configuration."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
number = config.get(CONF_NUMBER)
held_more_than = config.get(CONF_HELD_MORE_THAN)
held_less_than = config.get(CONF_HELD_LESS_THAN)
@@ -46,12 +46,12 @@ async def async_attach_trigger(hass, config, action, automation_info):
job,
{
"trigger": {
+ **trigger_data,
CONF_PLATFORM: "litejet",
CONF_NUMBER: number,
CONF_HELD_MORE_THAN: held_more_than,
CONF_HELD_LESS_THAN: held_less_than,
"description": f"litejet switch #{number}",
- "id": trigger_id,
}
},
)
diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py
index 424a6a92aba..25500a1fcfb 100644
--- a/homeassistant/components/litterrobot/__init__.py
+++ b/homeassistant/components/litterrobot/__init__.py
@@ -12,7 +12,7 @@ from .hub import LitterRobotHub
PLATFORMS = ["sensor", "switch", "vacuum"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Litter-Robot from a config entry."""
hass.data.setdefault(DOMAIN, {})
hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data)
diff --git a/homeassistant/components/litterrobot/translations/de.json b/homeassistant/components/litterrobot/translations/de.json
index 0eee2778d05..c8f4f35716e 100644
--- a/homeassistant/components/litterrobot/translations/de.json
+++ b/homeassistant/components/litterrobot/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ "already_configured": "Konto ist bereits konfiguriert"
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
diff --git a/homeassistant/components/litterrobot/translations/he.json b/homeassistant/components/litterrobot/translations/he.json
new file mode 100644
index 00000000000..454b7e1ae51
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/he.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py
index c4e8c541e4a..e97e5da7d49 100644
--- a/homeassistant/components/local_ip/__init__.py
+++ b/homeassistant/components/local_ip/__init__.py
@@ -8,12 +8,12 @@ from .const import DOMAIN, PLATFORMS
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up local_ip from a config entry."""
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/local_ip/config_flow.py b/homeassistant/components/local_ip/config_flow.py
index 2bc994c4dca..27bd5340d40 100644
--- a/homeassistant/components/local_ip/config_flow.py
+++ b/homeassistant/components/local_ip/config_flow.py
@@ -1,18 +1,23 @@
"""Config flow for local_ip."""
+from __future__ import annotations
-from homeassistant import config_entries
+from typing import Any
+
+from homeassistant.config_entries import ConfigFlow
+from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN
-class SimpleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+class SimpleConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for local_ip."""
VERSION = 1
- async def async_step_user(self, user_input=None):
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
"""Handle the initial step."""
-
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py
index 1d2cce72105..c7bc53caa69 100644
--- a/homeassistant/components/local_ip/sensor.py
+++ b/homeassistant/components/local_ip/sensor.py
@@ -1,46 +1,35 @@
"""Sensor platform for local_ip."""
from homeassistant.components.sensor import SensorEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import get_local_ip
from .const import DOMAIN, SENSOR
-async def async_setup_entry(hass, config_entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
"""Set up the platform from config_entry."""
- name = config_entry.data.get(CONF_NAME) or DOMAIN
+ name = entry.data.get(CONF_NAME) or DOMAIN
async_add_entities([IPSensor(name)], True)
class IPSensor(SensorEntity):
"""A simple sensor."""
- def __init__(self, name):
+ _attr_unique_id = SENSOR
+ _attr_icon = "mdi:ip"
+
+ def __init__(self, name: str) -> None:
"""Initialize the sensor."""
- self._state = None
- self._name = name
+ self._attr_name = name
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def unique_id(self):
- """Return the unique id of the sensor."""
- return SENSOR
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def icon(self):
- """Return the icon of the sensor."""
- return "mdi:ip"
-
- def update(self):
+ def update(self) -> None:
"""Fetch new state data for the sensor."""
- self._state = get_local_ip()
+ self._attr_state = get_local_ip()
diff --git a/homeassistant/components/local_ip/translations/he.json b/homeassistant/components/local_ip/translations/he.json
new file mode 100644
index 00000000000..08506bf3437
--- /dev/null
+++ b/homeassistant/components/local_ip/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "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?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/locative/translations/he.json b/homeassistant/components/locative/translations/he.json
new file mode 100644
index 00000000000..7e155c6bdd7
--- /dev/null
+++ b/homeassistant/components/locative/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.",
+ "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook."
+ },
+ "step": {
+ "user": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py
index 237daedae80..9e8bf3a740c 100644
--- a/homeassistant/components/lock/__init__.py
+++ b/homeassistant/components/lock/__init__.py
@@ -1,11 +1,14 @@
"""Component to interface with locks that can be controlled remotely."""
+from __future__ import annotations
+
from datetime import timedelta
import functools as ft
import logging
-from typing import final
+from typing import Any, final
import voluptuous as vol
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CODE,
ATTR_CODE_FORMAT,
@@ -15,6 +18,7 @@ from homeassistant.const import (
STATE_LOCKED,
STATE_UNLOCKED,
)
+from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
@@ -23,8 +27,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
-
-# mypy: allow-untyped-defs, no-check-untyped-defs
+from homeassistant.helpers.typing import ConfigType, StateType
_LOGGER = logging.getLogger(__name__)
@@ -45,7 +48,7 @@ SUPPORT_OPEN = 1
PROP_TO_ATTR = {"changed_by": ATTR_CHANGED_BY, "code_format": ATTR_CODE_FORMAT}
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Track states and offer events for locks."""
component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
@@ -66,61 +69,68 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class LockEntity(Entity):
"""Base class for lock entities."""
+ _attr_changed_by: str | None = None
+ _attr_code_format: str | None = None
+ _attr_is_locked: bool | None = None
+ _attr_state: None = None
+
@property
- def changed_by(self):
+ def changed_by(self) -> str | None:
"""Last change triggered by."""
- return None
+ return self._attr_changed_by
@property
- def code_format(self):
+ def code_format(self) -> str | None:
"""Regex for code format or None if no code is required."""
- return None
+ return self._attr_code_format
@property
- def is_locked(self):
+ def is_locked(self) -> bool | None:
"""Return true if the lock is locked."""
- return None
+ return self._attr_is_locked
- def lock(self, **kwargs):
+ def lock(self, **kwargs: Any) -> None:
"""Lock the lock."""
raise NotImplementedError()
- async def async_lock(self, **kwargs):
+ async def async_lock(self, **kwargs: Any) -> None:
"""Lock the lock."""
await self.hass.async_add_executor_job(ft.partial(self.lock, **kwargs))
- def unlock(self, **kwargs):
+ def unlock(self, **kwargs: Any) -> None:
"""Unlock the lock."""
raise NotImplementedError()
- async def async_unlock(self, **kwargs):
+ async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the lock."""
await self.hass.async_add_executor_job(ft.partial(self.unlock, **kwargs))
- def open(self, **kwargs):
+ def open(self, **kwargs: Any) -> None:
"""Open the door latch."""
raise NotImplementedError()
- async def async_open(self, **kwargs):
+ async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
await self.hass.async_add_executor_job(ft.partial(self.open, **kwargs))
@final
@property
- def state_attributes(self):
+ def state_attributes(self) -> dict[str, StateType]:
"""Return the state attributes."""
state_attr = {}
for prop, attr in PROP_TO_ATTR.items():
@@ -129,8 +139,9 @@ class LockEntity(Entity):
state_attr[attr] = value
return state_attr
+ @final
@property
- def state(self):
+ def state(self) -> str | None:
"""Return the state."""
locked = self.is_locked
if locked is None:
@@ -141,9 +152,9 @@ class LockEntity(Entity):
class LockDevice(LockEntity):
"""Representation of a lock (for backwards compatibility)."""
- def __init_subclass__(cls, **kwargs):
+ def __init_subclass__(cls, **kwargs: Any):
"""Print deprecation warning."""
- super().__init_subclass__(**kwargs)
+ super().__init_subclass__(**kwargs) # type: ignore[call-arg]
_LOGGER.warning(
"LockDevice is deprecated, modify %s to extend LockEntity",
cls.__name__,
diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py
index cb0e2b0daad..6c0eb2a41d4 100644
--- a/homeassistant/components/lock/device_action.py
+++ b/homeassistant/components/lock/device_action.py
@@ -5,7 +5,6 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
- ATTR_SUPPORTED_FEATURES,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
@@ -17,6 +16,7 @@ from homeassistant.const import (
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import get_supported_features
from . import DOMAIN, SUPPORT_OPEN
@@ -40,36 +40,20 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
if entry.domain != DOMAIN:
continue
- # Add actions for each entity that belongs to this integration
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "lock",
- }
- )
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "unlock",
- }
- )
+ supported_features = get_supported_features(hass, entry.entity_id)
- state = hass.states.get(entry.entity_id)
- if state:
- features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
- if features & (SUPPORT_OPEN):
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "open",
- }
- )
+ # Add actions for each entity that belongs to this integration
+ base_action = {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
+ actions.append({**base_action, CONF_TYPE: "lock"})
+ actions.append({**base_action, CONF_TYPE: "unlock"})
+
+ if supported_features & (SUPPORT_OPEN):
+ actions.append({**base_action, CONF_TYPE: "open"})
return actions
diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py
index 0fae680f829..3e77a23ffdb 100644
--- a/homeassistant/components/lock/device_condition.py
+++ b/homeassistant/components/lock/device_condition.py
@@ -41,24 +41,14 @@ async def async_get_conditions(hass: HomeAssistant, device_id: str) -> list[dict
continue
# Add conditions for each entity that belongs to this integration
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_locked",
- }
- )
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_unlocked",
- }
- )
+ base_condition = {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
+ conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES]
return conditions
diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py
index 77eb04e3735..2e96b470893 100644
--- a/homeassistant/components/lock/device_trigger.py
+++ b/homeassistant/components/lock/device_trigger.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import state as state_trigger
from homeassistant.const import (
CONF_DEVICE_ID,
@@ -24,7 +24,7 @@ from . import DOMAIN
TRIGGER_TYPES = {"locked", "unlocked"}
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
diff --git a/homeassistant/components/logi_circle/translations/de.json b/homeassistant/components/logi_circle/translations/de.json
index 1eec1d3c4a5..b1f318a8e5f 100644
--- a/homeassistant/components/logi_circle/translations/de.json
+++ b/homeassistant/components/logi_circle/translations/de.json
@@ -13,7 +13,7 @@
},
"step": {
"auth": {
- "description": "Folge dem Link unten und klicke Akzeptieren um auf dein Logi Circle-Konto zuzugreifen. Kehre dann zur\u00fcck und dr\u00fccke unten auf Senden . \n\n [Link] ({authorization_url})",
+ "description": "Folge dem Link unten und klicke **Akzeptieren** um auf dein Logi Circle-Konto zuzugreifen. Kehre dann zur\u00fcck und dr\u00fccke unten auf **Senden** . \n\n [Link] ({authorization_url})",
"title": "Authentifizierung mit Logi Circle"
},
"user": {
diff --git a/homeassistant/components/logi_circle/translations/he.json b/homeassistant/components/logi_circle/translations/he.json
new file mode 100644
index 00000000000..425a68fee00
--- /dev/null
+++ b/homeassistant/components/logi_circle/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3."
+ },
+ "error": {
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/he.json b/homeassistant/components/lovelace/translations/he.json
new file mode 100644
index 00000000000..45098b52165
--- /dev/null
+++ b/homeassistant/components/lovelace/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "system_health": {
+ "info": {
+ "dashboards": "\u05dc\u05d5\u05d7\u05d5\u05ea \u05d1\u05e7\u05e8\u05d4",
+ "mode": "\u05de\u05e6\u05d1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/luftdaten/translations/he.json b/homeassistant/components/luftdaten/translations/he.json
new file mode 100644
index 00000000000..11a4c93b42a
--- /dev/null
+++ b/homeassistant/components/luftdaten/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py
index 6fb394d333c..db5aa5dcccc 100644
--- a/homeassistant/components/lutron/binary_sensor.py
+++ b/homeassistant/components/lutron/binary_sensor.py
@@ -29,17 +29,14 @@ class LutronOccupancySensor(LutronDevice, BinarySensorEntity):
reported as a single occupancy group.
"""
+ _attr_device_class = DEVICE_CLASS_OCCUPANCY
+
@property
def is_on(self):
"""Return true if the binary sensor is on."""
# Error cases will end up treated as unoccupied.
return self._lutron_device.state == OccupancyGroup.State.OCCUPIED
- @property
- def device_class(self):
- """Return that this is an occupancy sensor."""
- return DEVICE_CLASS_OCCUPANCY
-
@property
def name(self):
"""Return the name of the device."""
diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py
index 230301c12f2..7d9728a79a1 100644
--- a/homeassistant/components/lutron_caseta/device_trigger.py
+++ b/homeassistant/components/lutron_caseta/device_trigger.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.device_automation.exceptions import (
InvalidDeviceAutomationConfig,
)
@@ -33,7 +33,7 @@ from .const import (
SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE]
-LUTRON_BUTTON_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): vol.In(SUPPORTED_INPUTS_EVENTS_TYPES),
}
diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json
index 1950edd8ff2..e87d4cc0bdb 100644
--- a/homeassistant/components/lutron_caseta/translations/de.json
+++ b/homeassistant/components/lutron_caseta/translations/de.json
@@ -8,13 +8,14 @@
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
},
- "flow_title": "Lutron Cas\u00e9ta {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"import_failed": {
"description": "Konnte die aus configuration.yaml importierte Bridge (Host: {host}) nicht einrichten.",
"title": "Import der Cas\u00e9ta-Bridge-Konfiguration fehlgeschlagen."
},
"link": {
+ "description": "Um ein Pairing mit {name} ({host}) durchzuf\u00fchren, dr\u00fccken Sie nach dem Absenden dieses Formulars die schwarze Taste auf der R\u00fcckseite der Br\u00fccke.",
"title": "Mit der Bridge verbinden"
},
"user": {
@@ -32,6 +33,20 @@
"button_2": "Zweite Taste",
"button_3": "Dritte Taste",
"button_4": "Vierte Taste",
+ "close_1": "Einen schlie\u00dfen",
+ "close_2": "Zwei schlie\u00dfen",
+ "close_3": "Drei schlie\u00dfen",
+ "close_4": "Vier schlie\u00dfen",
+ "close_all": "Alle schlie\u00dfen",
+ "group_1_button_1": "Erste Gruppe erste Taste",
+ "group_1_button_2": "Erste Gruppe zweite Taste",
+ "group_2_button_1": "Zweite Gruppe erste Taste",
+ "group_2_button_2": "Zweite Gruppe zweite Taste",
+ "lower": "Unter",
+ "lower_1": "Unterer",
+ "lower_2": "Untere zwei",
+ "lower_3": "Untere drei",
+ "lower_4": "Untere vier",
"lower_all": "Alle senken",
"off": "Aus",
"on": "An",
diff --git a/homeassistant/components/lutron_caseta/translations/he.json b/homeassistant/components/lutron_caseta/translations/he.json
index 7b55b0743fb..cb742b61b72 100644
--- a/homeassistant/components/lutron_caseta/translations/he.json
+++ b/homeassistant/components/lutron_caseta/translations/he.json
@@ -1,7 +1,24 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{name} ({host})",
"step": {
+ "import_failed": {
+ "description": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d2\u05e9\u05e8 (\u05de\u05d0\u05e8\u05d7: {host}) \u05d4\u05de\u05d9\u05d5\u05d1\u05d0 \u05de-configuration.yaml."
+ },
+ "link": {
+ "description": "\u05db\u05d3\u05d9 \u05dc\u05d6\u05d5\u05d5\u05d2 \u05e2\u05dd {name} ({host}), \u05dc\u05d0\u05d7\u05e8 \u05e9\u05dc\u05d9\u05d7\u05ea \u05d8\u05d5\u05e4\u05e1 \u05d6\u05d4, \u05dc\u05d7\u05e5 \u05e2\u05dc \u05d4\u05dc\u05d7\u05e6\u05df \u05d4\u05e9\u05d7\u05d5\u05e8 \u05d1\u05d2\u05d1 \u05d4\u05d2\u05e9\u05e8."
+ },
"user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ },
"description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4- IP \u05e9\u05dc \u05d4\u05de\u05db\u05e9\u05d9\u05e8."
}
}
diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json
index 921f5e83409..a8e62b37390 100644
--- a/homeassistant/components/lutron_caseta/translations/hu.json
+++ b/homeassistant/components/lutron_caseta/translations/hu.json
@@ -7,6 +7,7 @@
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/lyric/translations/he.json b/homeassistant/components/lyric/translations/he.json
new file mode 100644
index 00000000000..63428decee3
--- /dev/null
+++ b/homeassistant/components/lyric/translations/he.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "create_entry": {
+ "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ },
+ "reauth_confirm": {
+ "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/translations/he.json b/homeassistant/components/mailgun/translations/he.json
new file mode 100644
index 00000000000..ebee9aee976
--- /dev/null
+++ b/homeassistant/components/mailgun/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "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.",
+ "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py
index 2c480aaf606..d704cfb7f44 100644
--- a/homeassistant/components/mazda/__init__.py
+++ b/homeassistant/components/mazda/__init__.py
@@ -43,7 +43,7 @@ async def with_timeout(task, timeout_seconds=10):
return await task
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Mazda Connected Services from a config entry."""
email = entry.data[CONF_EMAIL]
password = entry.data[CONF_PASSWORD]
@@ -69,16 +69,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Handle a service call."""
# Get device entry from device registry
dev_reg = device_registry.async_get(hass)
- device_id = service_call.data.get("device_id")
+ device_id = service_call.data["device_id"]
device_entry = dev_reg.async_get(device_id)
# Get vehicle VIN from device identifiers
- mazda_identifiers = [
+ mazda_identifiers = (
identifier
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
- ]
- vin_identifier = next(iter(mazda_identifiers))
+ )
+ vin_identifier = next(mazda_identifiers)
vin = vin_identifier[1]
# Get vehicle ID and API client from hass.data
@@ -89,6 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if vehicle["vin"] == vin:
vehicle_id = vehicle["id"]
api_client = entry_data[DATA_CLIENT]
+ break
if vehicle_id == 0 or api_client is None:
raise HomeAssistantError("Vehicle ID not found")
@@ -96,14 +97,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
api_method = getattr(api_client, service_call.service)
try:
if service_call.service == "send_poi":
- latitude = service_call.data.get("latitude")
- longitude = service_call.data.get("longitude")
- poi_name = service_call.data.get("poi_name")
+ latitude = service_call.data["latitude"]
+ longitude = service_call.data["longitude"]
+ poi_name = service_call.data["poi_name"]
await api_method(vehicle_id, latitude, longitude, poi_name)
else:
await api_method(vehicle_id)
except Exception as ex:
- _LOGGER.exception("Error occurred during Mazda service call: %s", ex)
raise HomeAssistantError(ex) from ex
def validate_mazda_device_id(device_id):
@@ -119,7 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
]
- if len(mazda_identifiers) < 1:
+ if not mazda_identifiers:
raise vol.Invalid("Device ID is not a Mazda vehicle")
return device_id
diff --git a/homeassistant/components/mazda/translations/he.json b/homeassistant/components/mazda/translations/he.json
new file mode 100644
index 00000000000..9856fb9034c
--- /dev/null
+++ b/homeassistant/components/mazda/translations/he.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "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": {
+ "account_locked": "\u05d7\u05e9\u05d1\u05d5\u05df \u05e0\u05e2\u05d5\u05dc. \u05e0\u05d0 \u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1 \u05de\u05d0\u05d5\u05d7\u05e8 \u05d9\u05d5\u05ea\u05e8.",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u05d3\u05d5\u05d0\"\u05dc",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d3\u05d5\u05d0\"\u05dc \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d1\u05d4\u05df \u05d0\u05ea\u05d4 \u05de\u05e9\u05ea\u05de\u05e9 \u05db\u05d3\u05d9 \u05dc\u05d4\u05d9\u05db\u05e0\u05e1 \u05dc\u05d9\u05d9\u05e9\u05d5\u05dd MyMazda \u05dc\u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05e0\u05d9\u05d9\u05d3\u05d9\u05dd."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index 6fca2a4c3d5..ffdabe6fed7 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -5,7 +5,7 @@ import asyncio
import base64
import collections
from contextlib import suppress
-from datetime import timedelta
+import datetime as dt
import functools as ft
import hashlib
import logging
@@ -27,6 +27,7 @@ from homeassistant.components.websocket_api.const import (
ERR_NOT_SUPPORTED,
ERR_UNKNOWN_ERROR,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
HTTP_INTERNAL_SERVER_ERROR,
HTTP_NOT_FOUND,
@@ -52,11 +53,13 @@ from homeassistant.const import (
STATE_OFF,
STATE_PLAYING,
)
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
+ datetime,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
@@ -137,7 +140,7 @@ CACHE_URL = "url"
CACHE_CONTENT = "content"
ENTITY_IMAGE_CACHE = {CACHE_IMAGES: collections.OrderedDict(), CACHE_MAXSIZE: 16}
-SCAN_INTERVAL = timedelta(seconds=10)
+SCAN_INTERVAL = dt.timedelta(seconds=10)
DEVICE_CLASS_TV = "tv"
DEVICE_CLASS_SPEAKER = "speaker"
@@ -356,14 +359,16 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class MediaPlayerEntity(Entity):
@@ -371,11 +376,43 @@ class MediaPlayerEntity(Entity):
_access_token: str | None = None
+ _attr_app_id: str | None = None
+ _attr_app_name: str | None = None
+ _attr_group_members: list[str] | None = None
+ _attr_is_volume_muted: bool | None = None
+ _attr_media_album_artist: str | None = None
+ _attr_media_album_name: str | None = None
+ _attr_media_artist: str | None = None
+ _attr_media_channel: str | None = None
+ _attr_media_content_id: str | None = None
+ _attr_media_content_type: str | None = None
+ _attr_media_duration: int | None = None
+ _attr_media_episode: str | None = None
+ _attr_media_image_hash: str | None
+ _attr_media_image_remotely_accessible: bool = False
+ _attr_media_image_url: str | None = None
+ _attr_media_playlist: str | None = None
+ _attr_media_position_updated_at: dt.datetime | None = None
+ _attr_media_position: int | None = None
+ _attr_media_season: str | None = None
+ _attr_media_series_title: str | None = None
+ _attr_media_title: str | None = None
+ _attr_media_track: int | None = None
+ _attr_repeat: str | None = None
+ _attr_shuffle: bool | None = None
+ _attr_sound_mode_list: list[str] | None = None
+ _attr_sound_mode: str | None = None
+ _attr_source_list: list[str] | None = None
+ _attr_source: str | None = None
+ _attr_state: str | None = None
+ _attr_supported_features: int = 0
+ _attr_volume_level: float | None = None
+
# Implement these for your media player
@property
- def state(self):
+ def state(self) -> str | None:
"""State of the player."""
- return None
+ return self._attr_state
@property
def access_token(self) -> str:
@@ -385,56 +422,59 @@ class MediaPlayerEntity(Entity):
return self._access_token
@property
- def volume_level(self):
+ def volume_level(self) -> float | None:
"""Volume level of the media player (0..1)."""
- return None
+ return self._attr_volume_level
@property
- def is_volume_muted(self):
+ def is_volume_muted(self) -> bool | None:
"""Boolean if volume is currently muted."""
- return None
+ return self._attr_is_volume_muted
@property
- def media_content_id(self):
+ def media_content_id(self) -> str | None:
"""Content ID of current playing media."""
- return None
+ return self._attr_media_content_id
@property
- def media_content_type(self):
+ def media_content_type(self) -> str | None:
"""Content type of current playing media."""
- return None
+ return self._attr_media_content_type
@property
- def media_duration(self):
+ def media_duration(self) -> int | None:
"""Duration of current playing media in seconds."""
- return None
+ return self._attr_media_duration
@property
- def media_position(self):
+ def media_position(self) -> int | None:
"""Position of current playing media in seconds."""
- return None
+ return self._attr_media_position
@property
- def media_position_updated_at(self):
+ def media_position_updated_at(self) -> dt.datetime | None:
"""When was the position of the current playing media valid.
Returns value from homeassistant.util.dt.utcnow().
"""
- return None
+ return self._attr_media_position_updated_at
@property
- def media_image_url(self):
+ def media_image_url(self) -> str | None:
"""Image url of current playing media."""
- return None
+ return self._attr_media_image_url
@property
def media_image_remotely_accessible(self) -> bool:
"""If the image url is remotely accessible."""
- return False
+ return self._attr_media_image_remotely_accessible
@property
- def media_image_hash(self):
+ def media_image_hash(self) -> str | None:
"""Hash value for media image."""
+ if hasattr(self, "_attr_media_image_hash"):
+ return self._attr_media_image_hash
+
url = self.media_image_url
if url is not None:
return hashlib.sha256(url.encode("utf-8")).hexdigest()[:16]
@@ -463,104 +503,104 @@ class MediaPlayerEntity(Entity):
return None, None
@property
- def media_title(self):
+ def media_title(self) -> str | None:
"""Title of current playing media."""
- return None
+ return self._attr_media_title
@property
- def media_artist(self):
+ def media_artist(self) -> str | None:
"""Artist of current playing media, music track only."""
- return None
+ return self._attr_media_artist
@property
- def media_album_name(self):
+ def media_album_name(self) -> str | None:
"""Album name of current playing media, music track only."""
- return None
+ return self._attr_media_album_name
@property
- def media_album_artist(self):
+ def media_album_artist(self) -> str | None:
"""Album artist of current playing media, music track only."""
- return None
+ return self._attr_media_album_artist
@property
- def media_track(self):
+ def media_track(self) -> int | None:
"""Track number of current playing media, music track only."""
- return None
+ return self._attr_media_track
@property
- def media_series_title(self):
+ def media_series_title(self) -> str | None:
"""Title of series of current playing media, TV show only."""
- return None
+ return self._attr_media_series_title
@property
- def media_season(self):
+ def media_season(self) -> str | None:
"""Season of current playing media, TV show only."""
- return None
+ return self._attr_media_season
@property
- def media_episode(self):
+ def media_episode(self) -> str | None:
"""Episode of current playing media, TV show only."""
- return None
+ return self._attr_media_episode
@property
- def media_channel(self):
+ def media_channel(self) -> str | None:
"""Channel currently playing."""
- return None
+ return self._attr_media_channel
@property
- def media_playlist(self):
+ def media_playlist(self) -> str | None:
"""Title of Playlist currently playing."""
- return None
+ return self._attr_media_playlist
@property
- def app_id(self):
+ def app_id(self) -> str | None:
"""ID of the current running app."""
- return None
+ return self._attr_app_id
@property
- def app_name(self):
+ def app_name(self) -> str | None:
"""Name of the current running app."""
- return None
+ return self._attr_app_name
@property
- def source(self):
+ def source(self) -> str | None:
"""Name of the current input source."""
- return None
+ return self._attr_source
@property
- def source_list(self):
+ def source_list(self) -> list[str] | None:
"""List of available input sources."""
- return None
+ return self._attr_source_list
@property
- def sound_mode(self):
+ def sound_mode(self) -> str | None:
"""Name of the current sound mode."""
- return None
+ return self._attr_sound_mode
@property
- def sound_mode_list(self):
+ def sound_mode_list(self) -> list[str] | None:
"""List of available sound modes."""
- return None
+ return self._attr_sound_mode_list
@property
- def shuffle(self):
+ def shuffle(self) -> bool | None:
"""Boolean if shuffle is enabled."""
- return None
+ return self._attr_shuffle
@property
- def repeat(self):
+ def repeat(self) -> str | None:
"""Return current repeat mode."""
- return None
+ return self._attr_repeat
@property
- def group_members(self):
+ def group_members(self) -> list[str] | None:
"""List of members which are currently grouped together."""
- return None
+ return self._attr_group_members
@property
- def supported_features(self):
+ def supported_features(self) -> int:
"""Flag media player features that are supported."""
- return 0
+ return self._attr_supported_features
def turn_on(self):
"""Turn the media player on."""
diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py
index 0e6e0f96c40..e392c274f33 100644
--- a/homeassistant/components/media_player/device_condition.py
+++ b/homeassistant/components/media_player/device_condition.py
@@ -46,51 +46,14 @@ async def async_get_conditions(
continue
# Add conditions for each entity that belongs to this integration
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_on",
- }
- )
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_off",
- }
- )
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_idle",
- }
- )
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_paused",
- }
- )
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_playing",
- }
- )
+ base_condition = {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
+ conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES]
return conditions
diff --git a/homeassistant/components/media_player/device_trigger.py b/homeassistant/components/media_player/device_trigger.py
index 889bc776962..de0ff6b8e90 100644
--- a/homeassistant/components/media_player/device_trigger.py
+++ b/homeassistant/components/media_player/device_trigger.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import state as state_trigger
from homeassistant.const import (
CONF_DEVICE_ID,
@@ -27,7 +27,7 @@ from . import DOMAIN
TRIGGER_TYPES = {"turned_on", "turned_off", "idle", "paused", "playing"}
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
diff --git a/homeassistant/components/media_player/translations/he.json b/homeassistant/components/media_player/translations/he.json
index f29b3add414..8efa77488ba 100644
--- a/homeassistant/components/media_player/translations/he.json
+++ b/homeassistant/components/media_player/translations/he.json
@@ -1,9 +1,25 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_idle": "{entity_name} \u05de\u05de\u05ea\u05d9\u05df",
+ "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9",
+ "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc",
+ "is_paused": "{entity_name} \u05de\u05d5\u05e9\u05d4\u05d4",
+ "is_playing": "{entity_name} \u05de\u05ea\u05e0\u05d2\u05df"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} \u05d4\u05d5\u05e4\u05da \u05dc\u05de\u05de\u05ea\u05d9\u05df",
+ "paused": "{entity_name} \u05de\u05d5\u05e9\u05d4\u05d4",
+ "playing": "{entity_name} \u05de\u05ea\u05d7\u05d9\u05dc \u05dc\u05e0\u05d2\u05df",
+ "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4",
+ "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc"
+ }
+ },
"state": {
"_": {
"idle": "\u05de\u05de\u05ea\u05d9\u05df",
"off": "\u05db\u05d1\u05d5\u05d9",
- "on": "\u05d3\u05dc\u05d5\u05e7",
+ "on": "\u05de\u05d5\u05e4\u05e2\u05dc",
"paused": "\u05de\u05d5\u05e9\u05d4\u05d4",
"playing": "\u05de\u05e0\u05d2\u05df",
"standby": "\u05de\u05e6\u05d1 \u05d4\u05de\u05ea\u05e0\u05d4"
diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py
index 7b42c1f42e8..12b80554933 100644
--- a/homeassistant/components/melcloud/__init__.py
+++ b/homeassistant/components/melcloud/__init__.py
@@ -61,7 +61,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigEntry):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Establish connection with MELClooud."""
conf = entry.data
mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN])
diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py
index 42c5ed7ef85..98aeaf73be1 100644
--- a/homeassistant/components/melcloud/climate.py
+++ b/homeassistant/components/melcloud/climate.py
@@ -100,11 +100,12 @@ async def async_setup_entry(
class MelCloudClimate(ClimateEntity):
"""Base climate device."""
+ _attr_temperature_unit = TEMP_CELSIUS
+
def __init__(self, device: MelCloudDevice) -> None:
"""Initialize the climate."""
self.api = device
self._base_device = self.api.device
- self._name = device.name
async def async_update(self):
"""Update state from MELCloud."""
@@ -124,20 +125,17 @@ class MelCloudClimate(ClimateEntity):
class AtaDeviceClimate(MelCloudClimate):
"""Air-to-Air climate device."""
+ _attr_supported_features = (
+ SUPPORT_FAN_MODE | SUPPORT_TARGET_TEMPERATURE | SUPPORT_SWING_MODE
+ )
+
def __init__(self, device: MelCloudDevice, ata_device: AtaDevice) -> None:
"""Initialize the climate."""
super().__init__(device)
self._device = ata_device
- @property
- def unique_id(self) -> str | None:
- """Return a unique ID."""
- return f"{self.api.device.serial}-{self.api.device.mac}"
-
- @property
- def name(self):
- """Return the display name of this entity."""
- return self._name
+ self._attr_name = device.name
+ self._attr_unique_id = f"{self.api.device.serial}-{self.api.device.mac}"
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
@@ -163,11 +161,6 @@ class AtaDeviceClimate(MelCloudClimate):
)
return attr
- @property
- def temperature_unit(self) -> str:
- """Return the unit of measurement used by the platform."""
- return TEMP_CELSIUS
-
@property
def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode."""
@@ -258,11 +251,6 @@ class AtaDeviceClimate(MelCloudClimate):
"""Return a list of available vertical vane positions and modes."""
return self._device.vane_vertical_positions
- @property
- def supported_features(self) -> int:
- """Return the list of supported features."""
- return SUPPORT_FAN_MODE | SUPPORT_TARGET_TEMPERATURE | SUPPORT_SWING_MODE
-
async def async_turn_on(self) -> None:
"""Turn the entity on."""
await self._device.set({"power": True})
@@ -293,6 +281,10 @@ class AtaDeviceClimate(MelCloudClimate):
class AtwDeviceZoneClimate(MelCloudClimate):
"""Air-to-Water zone climate device."""
+ _attr_max_temp = 30
+ _attr_min_temp = 10
+ _attr_supported_features = SUPPORT_TARGET_TEMPERATURE
+
def __init__(
self, device: MelCloudDevice, atw_device: AtwDevice, atw_zone: Zone
) -> None:
@@ -301,15 +293,8 @@ class AtwDeviceZoneClimate(MelCloudClimate):
self._device = atw_device
self._zone = atw_zone
- @property
- def unique_id(self) -> str | None:
- """Return a unique ID."""
- return f"{self.api.device.serial}-{self._zone.zone_index}"
-
- @property
- def name(self) -> str:
- """Return the display name of this entity."""
- return f"{self._name} {self._zone.name}"
+ self._attr_name = f"{device.name} {self._zone.name}"
+ self._attr_unique_id = f"{self.api.device.serial}-{atw_zone.zone_index}"
@property
def extra_state_attributes(self) -> dict[str, Any]:
@@ -321,11 +306,6 @@ class AtwDeviceZoneClimate(MelCloudClimate):
}
return data
- @property
- def temperature_unit(self) -> str:
- """Return the unit of measurement used by the platform."""
- return TEMP_CELSIUS
-
@property
def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode."""
@@ -372,24 +352,3 @@ class AtwDeviceZoneClimate(MelCloudClimate):
await self._zone.set_target_temperature(
kwargs.get("temperature", self.target_temperature)
)
-
- @property
- def supported_features(self) -> int:
- """Return the list of supported features."""
- return SUPPORT_TARGET_TEMPERATURE
-
- @property
- def min_temp(self) -> float:
- """Return the minimum temperature.
-
- MELCloud API does not expose radiator zone temperature limits.
- """
- return 10
-
- @property
- def max_temp(self) -> float:
- """Return the maximum temperature.
-
- MELCloud API does not expose radiator zone temperature limits.
- """
- return 30
diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py
index 356992ece11..c1b7e5e8cbd 100644
--- a/homeassistant/components/melcloud/sensor.py
+++ b/homeassistant/components/melcloud/sensor.py
@@ -2,14 +2,19 @@
from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW
from pymelcloud.atw_device import Zone
-from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.sensor import (
+ DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_TEMPERATURE,
+ STATE_CLASS_MEASUREMENT,
+ SensorEntity,
+)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
- DEVICE_CLASS_TEMPERATURE,
ENERGY_KILO_WATT_HOUR,
TEMP_CELSIUS,
)
+from homeassistant.util import dt as dt_util
from . import MelCloudDevice
from .const import DOMAIN
@@ -32,7 +37,7 @@ ATA_SENSORS = {
ATTR_MEASUREMENT_NAME: "Energy",
ATTR_ICON: "mdi:factory",
ATTR_UNIT: ENERGY_KILO_WATT_HOUR,
- ATTR_DEVICE_CLASS: None,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
ATTR_VALUE_FN: lambda x: x.device.total_energy_consumed,
ATTR_ENABLED_FN: lambda x: x.device.has_energy_consumed_meter,
},
@@ -116,40 +121,23 @@ class MelDeviceSensor(SensorEntity):
def __init__(self, api: MelCloudDevice, measurement, definition):
"""Initialize the sensor."""
self._api = api
- self._name_slug = api.name
- self._measurement = measurement
self._def = definition
- @property
- def unique_id(self):
- """Return a unique ID."""
- return f"{self._api.device.serial}-{self._api.device.mac}-{self._measurement}"
+ self._attr_device_class = definition[ATTR_DEVICE_CLASS]
+ self._attr_icon = definition[ATTR_ICON]
+ self._attr_name = f"{api.name} {definition[ATTR_MEASUREMENT_NAME]}"
+ self._attr_unique_id = f"{api.device.serial}-{api.device.mac}-{measurement}"
+ self._attr_unit_of_measurement = definition[ATTR_UNIT]
+ self._attr_state_class = STATE_CLASS_MEASUREMENT
- @property
- def icon(self):
- """Return the icon to use in the frontend, if any."""
- return self._def[ATTR_ICON]
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return f"{self._name_slug} {self._def[ATTR_MEASUREMENT_NAME]}"
+ if self.device_class == DEVICE_CLASS_ENERGY:
+ self._attr_last_reset = dt_util.utc_from_timestamp(0)
@property
def state(self):
"""Return the state of the sensor."""
return self._def[ATTR_VALUE_FN](self._api)
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return self._def[ATTR_UNIT]
-
- @property
- def device_class(self):
- """Return device class."""
- return self._def[ATTR_DEVICE_CLASS]
-
async def async_update(self):
"""Retrieve latest state."""
await self._api.async_update()
@@ -165,9 +153,13 @@ class AtwZoneSensor(MelDeviceSensor):
def __init__(self, api: MelCloudDevice, zone: Zone, measurement, definition):
"""Initialize the sensor."""
- super().__init__(api, measurement, definition)
+ if zone.zone_index == 1:
+ full_measurement = measurement
+ else:
+ full_measurement = f"{measurement}-zone-{zone.zone_index}"
+ super().__init__(api, full_measurement, definition)
self._zone = zone
- self._name_slug = f"{api.name} {zone.name}"
+ self._attr_name = f"{api.name} {zone.name} {definition[ATTR_MEASUREMENT_NAME]}"
@property
def state(self):
diff --git a/homeassistant/components/melcloud/translations/he.json b/homeassistant/components/melcloud/translations/he.json
index ac90b3264ea..d04ae43734b 100644
--- a/homeassistant/components/melcloud/translations/he.json
+++ b/homeassistant/components/melcloud/translations/he.json
@@ -1,10 +1,15 @@
{
"config": {
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
"user": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
- "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ "username": "\u05d3\u05d5\u05d0\"\u05dc"
}
}
}
diff --git a/homeassistant/components/met/translations/de.json b/homeassistant/components/met/translations/de.json
index e2bb171c749..0bc30508ab8 100644
--- a/homeassistant/components/met/translations/de.json
+++ b/homeassistant/components/met/translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_home": "In der Konfiguration von Home Assistant sind keine Home-Koordinaten eingestellt"
+ },
"error": {
"already_configured": "Der Dienst ist bereits konfiguriert"
},
diff --git a/homeassistant/components/met/translations/he.json b/homeassistant/components/met/translations/he.json
index 4c49313d977..f493f06b23c 100644
--- a/homeassistant/components/met/translations/he.json
+++ b/homeassistant/components/met/translations/he.json
@@ -1,10 +1,17 @@
{
"config": {
+ "error": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
"step": {
"user": {
"data": {
- "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
- }
+ "elevation": "\u05d2\u05d5\u05d1\u05d4",
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
+ "name": "\u05e9\u05dd"
+ },
+ "title": "\u05de\u05d9\u05e7\u05d5\u05dd"
}
}
}
diff --git a/homeassistant/components/met_eireann/translations/de.json b/homeassistant/components/met_eireann/translations/de.json
index 0d979ed800b..a7e1ff9668b 100644
--- a/homeassistant/components/met_eireann/translations/de.json
+++ b/homeassistant/components/met_eireann/translations/de.json
@@ -11,6 +11,7 @@
"longitude": "L\u00e4ngengrad",
"name": "Name"
},
+ "description": "Geben Sie Ihren Standort ein, um Wetterdaten von der Met \u00c9ireann Public Weather Forecast API zu verwenden",
"title": "Standort"
}
}
diff --git a/homeassistant/components/met_eireann/translations/he.json b/homeassistant/components/met_eireann/translations/he.json
new file mode 100644
index 00000000000..f493f06b23c
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/he.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "\u05d2\u05d5\u05d1\u05d4",
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
+ "name": "\u05e9\u05dd"
+ },
+ "title": "\u05de\u05d9\u05e7\u05d5\u05dd"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/translations/he.json b/homeassistant/components/meteo_france/translations/he.json
new file mode 100644
index 00000000000..9b05c651e27
--- /dev/null
+++ b/homeassistant/components/meteo_france/translations/he.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "cities": {
+ "data": {
+ "city": "\u05e2\u05d9\u05e8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json
index 0888a8fa063..ffdd7d8f49d 100644
--- a/homeassistant/components/meteoalarm/manifest.json
+++ b/homeassistant/components/meteoalarm/manifest.json
@@ -2,7 +2,7 @@
"domain": "meteoalarm",
"name": "MeteoAlarm",
"documentation": "https://www.home-assistant.io/integrations/meteoalarm",
- "requirements": ["meteoalertapi==0.1.6"],
+ "requirements": ["meteoalertapi==0.2.0"],
"codeowners": ["@rolfberkenbosch"],
- "iot_class": "local_polling"
+ "iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py
index 79e63e9b64d..58e51d0490a 100644
--- a/homeassistant/components/meteoclimatic/__init__.py
+++ b/homeassistant/components/meteoclimatic/__init__.py
@@ -5,7 +5,7 @@ from meteoclimatic import MeteoclimaticClient
from meteoclimatic.exceptions import MeteoclimaticError
from homeassistant.config_entries import ConfigEntry
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_STATION_CODE, DOMAIN, PLATFORMS, SCAN_INTERVAL
@@ -13,7 +13,7 @@ from .const import CONF_STATION_CODE, DOMAIN, PLATFORMS, SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a Meteoclimatic entry."""
station_code = entry.data[CONF_STATION_CODE]
meteoclimatic_client = MeteoclimaticClient()
@@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- name=f"Meteoclimatic Coordinator for {station_code}",
+ name=f"Meteoclimatic weather for {entry.title} ({station_code})",
update_method=async_update_data,
update_interval=SCAN_INTERVAL,
)
@@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
return True
-async def async_unload_entry(hass: HomeAssistantType, 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)
return unload_ok
diff --git a/homeassistant/components/meteoclimatic/const.py b/homeassistant/components/meteoclimatic/const.py
index eb3823a9b42..cd4be5821ea 100644
--- a/homeassistant/components/meteoclimatic/const.py
+++ b/homeassistant/components/meteoclimatic/const.py
@@ -34,8 +34,10 @@ from homeassistant.const import (
)
DOMAIN = "meteoclimatic"
-PLATFORMS = ["weather"]
+PLATFORMS = ["sensor", "weather"]
ATTRIBUTION = "Data provided by Meteoclimatic"
+MODEL = "Meteoclimatic RSS feed"
+MANUFACTURER = "Meteoclimatic"
SCAN_INTERVAL = timedelta(minutes=10)
@@ -54,12 +56,12 @@ SENSOR_TYPES = {
SENSOR_TYPE_CLASS: DEVICE_CLASS_TEMPERATURE,
},
"temp_max": {
- SENSOR_TYPE_NAME: "Max Temp.",
+ SENSOR_TYPE_NAME: "Daily Max Temperature",
SENSOR_TYPE_UNIT: TEMP_CELSIUS,
SENSOR_TYPE_CLASS: DEVICE_CLASS_TEMPERATURE,
},
"temp_min": {
- SENSOR_TYPE_NAME: "Min Temp.",
+ SENSOR_TYPE_NAME: "Daily Min Temperature",
SENSOR_TYPE_UNIT: TEMP_CELSIUS,
SENSOR_TYPE_CLASS: DEVICE_CLASS_TEMPERATURE,
},
@@ -69,12 +71,12 @@ SENSOR_TYPES = {
SENSOR_TYPE_CLASS: DEVICE_CLASS_HUMIDITY,
},
"humidity_max": {
- SENSOR_TYPE_NAME: "Max Humidity",
+ SENSOR_TYPE_NAME: "Daily Max Humidity",
SENSOR_TYPE_UNIT: PERCENTAGE,
SENSOR_TYPE_CLASS: DEVICE_CLASS_HUMIDITY,
},
"humidity_min": {
- SENSOR_TYPE_NAME: "Min Humidity",
+ SENSOR_TYPE_NAME: "Daily Min Humidity",
SENSOR_TYPE_UNIT: PERCENTAGE,
SENSOR_TYPE_CLASS: DEVICE_CLASS_HUMIDITY,
},
@@ -84,12 +86,12 @@ SENSOR_TYPES = {
SENSOR_TYPE_CLASS: DEVICE_CLASS_PRESSURE,
},
"pressure_max": {
- SENSOR_TYPE_NAME: "Max Pressure",
+ SENSOR_TYPE_NAME: "Daily Max Pressure",
SENSOR_TYPE_UNIT: PRESSURE_HPA,
SENSOR_TYPE_CLASS: DEVICE_CLASS_PRESSURE,
},
"pressure_min": {
- SENSOR_TYPE_NAME: "Min Pressure",
+ SENSOR_TYPE_NAME: "Daily Min Pressure",
SENSOR_TYPE_UNIT: PRESSURE_HPA,
SENSOR_TYPE_CLASS: DEVICE_CLASS_PRESSURE,
},
@@ -99,7 +101,7 @@ SENSOR_TYPES = {
SENSOR_TYPE_ICON: "mdi:weather-windy",
},
"wind_max": {
- SENSOR_TYPE_NAME: "Max Wind Speed",
+ SENSOR_TYPE_NAME: "Daily Max Wind Speed",
SENSOR_TYPE_UNIT: SPEED_KILOMETERS_PER_HOUR,
SENSOR_TYPE_ICON: "mdi:weather-windy",
},
@@ -109,9 +111,9 @@ SENSOR_TYPES = {
SENSOR_TYPE_ICON: "mdi:weather-windy",
},
"rain": {
- SENSOR_TYPE_NAME: "Rain",
+ SENSOR_TYPE_NAME: "Daily Precipitation",
SENSOR_TYPE_UNIT: LENGTH_MILLIMETERS,
- SENSOR_TYPE_ICON: "mdi:weather-rainy",
+ SENSOR_TYPE_ICON: "mdi:cup-water",
},
}
diff --git a/homeassistant/components/meteoclimatic/sensor.py b/homeassistant/components/meteoclimatic/sensor.py
new file mode 100644
index 00000000000..bcd597d4b0c
--- /dev/null
+++ b/homeassistant/components/meteoclimatic/sensor.py
@@ -0,0 +1,79 @@
+"""Support for Meteoclimatic sensor."""
+import logging
+
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import ATTR_ATTRIBUTION
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
+
+from .const import (
+ ATTRIBUTION,
+ DOMAIN,
+ MANUFACTURER,
+ MODEL,
+ SENSOR_TYPE_CLASS,
+ SENSOR_TYPE_ICON,
+ SENSOR_TYPE_NAME,
+ SENSOR_TYPE_UNIT,
+ SENSOR_TYPES,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up the Meteoclimatic sensor platform."""
+ coordinator = hass.data[DOMAIN][entry.entry_id]
+
+ async_add_entities(
+ [MeteoclimaticSensor(sensor_type, coordinator) for sensor_type in SENSOR_TYPES],
+ False,
+ )
+
+
+class MeteoclimaticSensor(CoordinatorEntity, SensorEntity):
+ """Representation of a Meteoclimatic sensor."""
+
+ def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator) -> None:
+ """Initialize the Meteoclimatic sensor."""
+ super().__init__(coordinator)
+ self._type = sensor_type
+ station = self.coordinator.data["station"]
+ self._attr_device_class = SENSOR_TYPES[sensor_type].get(SENSOR_TYPE_CLASS)
+ self._attr_icon = SENSOR_TYPES[sensor_type].get(SENSOR_TYPE_ICON)
+ self._attr_name = (
+ f"{station.name} {SENSOR_TYPES[sensor_type][SENSOR_TYPE_NAME]}"
+ )
+ self._attr_unique_id = f"{station.code}_{sensor_type}"
+ self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type].get(SENSOR_TYPE_UNIT)
+
+ @property
+ def device_info(self):
+ """Return the device info."""
+ return {
+ "identifiers": {(DOMAIN, self.platform.config_entry.unique_id)},
+ "name": self.coordinator.name,
+ "manufacturer": MANUFACTURER,
+ "model": MODEL,
+ "entry_type": "service",
+ }
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return (
+ getattr(self.coordinator.data["weather"], self._type)
+ if self.coordinator.data
+ else None
+ )
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/meteoclimatic/translations/ca.json b/homeassistant/components/meteoclimatic/translations/ca.json
new file mode 100644
index 00000000000..5f672d87535
--- /dev/null
+++ b/homeassistant/components/meteoclimatic/translations/ca.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "unknown": "Error inesperat"
+ },
+ "error": {
+ "not_found": "No s'han trobat dispositius a la xarxa"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "code": "Codi d'estaci\u00f3"
+ },
+ "description": "Introdueix el codi d'estaci\u00f3 meteorol\u00f2gica (per exemple, ESCAT4300000043206B)",
+ "title": "Meteoclimatic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteoclimatic/translations/de.json b/homeassistant/components/meteoclimatic/translations/de.json
new file mode 100644
index 00000000000..e23662146b2
--- /dev/null
+++ b/homeassistant/components/meteoclimatic/translations/de.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "error": {
+ "not_found": "Keine Ger\u00e4te im Netzwerk gefunden"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "code": "Stationscode"
+ },
+ "description": "Geben Sie den Code der Meteoclimatic-Station ein (z. B. ESCAT4300000043206B)",
+ "title": "Meteoclimatic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteoclimatic/translations/es.json b/homeassistant/components/meteoclimatic/translations/es.json
new file mode 100644
index 00000000000..251fcbe8e09
--- /dev/null
+++ b/homeassistant/components/meteoclimatic/translations/es.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "code": "C\u00f3digo de la estaci\u00f3n"
+ },
+ "description": "Introduzca el c\u00f3digo de la estaci\u00f3n Meteoclimatic (por ejemplo, ESCAT430000000043206B)",
+ "title": "Meteoclimatic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteoclimatic/translations/he.json b/homeassistant/components/meteoclimatic/translations/he.json
new file mode 100644
index 00000000000..db961a2f14c
--- /dev/null
+++ b/homeassistant/components/meteoclimatic/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",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "not_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "code": "\u05e7\u05d5\u05d3 \u05ea\u05d7\u05e0\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteoclimatic/translations/hu.json b/homeassistant/components/meteoclimatic/translations/hu.json
new file mode 100644
index 00000000000..893a6693c01
--- /dev/null
+++ b/homeassistant/components/meteoclimatic/translations/hu.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "error": {
+ "not_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteoclimatic/translations/it.json b/homeassistant/components/meteoclimatic/translations/it.json
new file mode 100644
index 00000000000..fcdfa25c496
--- /dev/null
+++ b/homeassistant/components/meteoclimatic/translations/it.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "unknown": "Errore imprevisto"
+ },
+ "error": {
+ "not_found": "Nessun dispositivo trovato sulla rete"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "code": "Codice della stazione"
+ },
+ "description": "Immettere il codice della stazione Meteoclimatic (ad esempio ESCAT4300000043206B)",
+ "title": "Meteoclimatic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteoclimatic/translations/nl.json b/homeassistant/components/meteoclimatic/translations/nl.json
new file mode 100644
index 00000000000..0b4aa397276
--- /dev/null
+++ b/homeassistant/components/meteoclimatic/translations/nl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "unknown": "Onverwachte fout"
+ },
+ "error": {
+ "not_found": "Geen apparaten gevonden op het netwerk"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "code": "Station code"
+ },
+ "description": "Voer de code van het meteoklimatologische station in (bv. ESCAT43000043206B)",
+ "title": "Meteoclimatic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteoclimatic/translations/no.json b/homeassistant/components/meteoclimatic/translations/no.json
new file mode 100644
index 00000000000..0e6e080d146
--- /dev/null
+++ b/homeassistant/components/meteoclimatic/translations/no.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "unknown": "Uventet feil"
+ },
+ "error": {
+ "not_found": "Ingen enheter funnet p\u00e5 nettverket"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "code": "Stasjonskode"
+ },
+ "description": "Angi Meteoclimatic stasjonskode (f.eks. ESCAT4300000043206B)",
+ "title": "Meteoclimatic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteoclimatic/translations/pl.json b/homeassistant/components/meteoclimatic/translations/pl.json
new file mode 100644
index 00000000000..c4539bd6c8b
--- /dev/null
+++ b/homeassistant/components/meteoclimatic/translations/pl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "error": {
+ "not_found": "Nie znaleziono urz\u0105dze\u0144 w sieci"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "code": "Kod stacji"
+ },
+ "description": "Wpisz kod stacji Meteoclimatic (np. ESCAT4300000043206B)",
+ "title": "Meteoclimatic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteoclimatic/translations/zh-Hant.json b/homeassistant/components/meteoclimatic/translations/zh-Hant.json
new file mode 100644
index 00000000000..d5c3793be7d
--- /dev/null
+++ b/homeassistant/components/meteoclimatic/translations/zh-Hant.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "error": {
+ "not_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "code": "\u6c23\u8c61\u7ad9\u4ee3\u78bc"
+ },
+ "description": "\u8f38\u5165 Meteoclimatic \u6c23\u8c61\u7ad9\u4ee3\u78bc\uff08\u4f8b\u5982 ESCAT4300000043206B\uff09",
+ "title": "Meteoclimatic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py
index 7059e935b2e..1326d700826 100644
--- a/homeassistant/components/meteoclimatic/weather.py
+++ b/homeassistant/components/meteoclimatic/weather.py
@@ -4,14 +4,14 @@ from meteoclimatic import Condition
from homeassistant.components.weather import WeatherEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TEMP_CELSIUS
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
-from .const import ATTRIBUTION, CONDITION_CLASSES, DOMAIN
+from .const import ATTRIBUTION, CONDITION_CLASSES, DOMAIN, MANUFACTURER, MODEL
def format_condition(condition):
@@ -25,7 +25,7 @@ def format_condition(condition):
async def async_setup_entry(
- hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Meteoclimatic weather platform."""
coordinator = hass.data[DOMAIN][entry.entry_id]
@@ -52,6 +52,17 @@ class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity):
"""Return the unique id of the sensor."""
return self._unique_id
+ @property
+ def device_info(self):
+ """Return the device info."""
+ return {
+ "identifiers": {(DOMAIN, self.platform.config_entry.unique_id)},
+ "name": self.coordinator.name,
+ "manufacturer": MANUFACTURER,
+ "model": MODEL,
+ "entry_type": "service",
+ }
+
@property
def condition(self):
"""Return the current condition."""
diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py
index 9bf9e44b72a..1a55c940d81 100644
--- a/homeassistant/components/metoffice/__init__.py
+++ b/homeassistant/components/metoffice/__init__.py
@@ -1,7 +1,10 @@
"""The Met Office integration."""
+import asyncio
import logging
+import datapoint
+
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant
@@ -11,18 +14,22 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import (
DEFAULT_SCAN_INTERVAL,
DOMAIN,
- METOFFICE_COORDINATOR,
- METOFFICE_DATA,
+ METOFFICE_COORDINATES,
+ METOFFICE_DAILY_COORDINATOR,
+ METOFFICE_HOURLY_COORDINATOR,
METOFFICE_NAME,
+ MODE_3HOURLY,
+ MODE_DAILY,
)
from .data import MetOfficeData
+from .helpers import fetch_data, fetch_site
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor", "weather"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a Met Office entry."""
latitude = entry.data[CONF_LATITUDE]
@@ -30,30 +37,53 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
api_key = entry.data[CONF_API_KEY]
site_name = entry.data[CONF_NAME]
- metoffice_data = MetOfficeData(hass, api_key, latitude, longitude)
- await metoffice_data.async_update_site()
- if metoffice_data.site_name is None:
+ connection = datapoint.connection(api_key=api_key)
+
+ site = await hass.async_add_executor_job(
+ fetch_site, connection, latitude, longitude
+ )
+ if site is None:
raise ConfigEntryNotReady()
- metoffice_coordinator = DataUpdateCoordinator(
+ async def async_update_3hourly() -> MetOfficeData:
+ return await hass.async_add_executor_job(
+ fetch_data, connection, site, MODE_3HOURLY
+ )
+
+ async def async_update_daily() -> MetOfficeData:
+ return await hass.async_add_executor_job(
+ fetch_data, connection, site, MODE_DAILY
+ )
+
+ metoffice_hourly_coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- name=f"MetOffice Coordinator for {site_name}",
- update_method=metoffice_data.async_update,
+ name=f"MetOffice Hourly Coordinator for {site_name}",
+ update_method=async_update_3hourly,
+ update_interval=DEFAULT_SCAN_INTERVAL,
+ )
+
+ metoffice_daily_coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=f"MetOffice Daily Coordinator for {site_name}",
+ update_method=async_update_daily,
update_interval=DEFAULT_SCAN_INTERVAL,
)
metoffice_hass_data = hass.data.setdefault(DOMAIN, {})
metoffice_hass_data[entry.entry_id] = {
- METOFFICE_DATA: metoffice_data,
- METOFFICE_COORDINATOR: metoffice_coordinator,
+ METOFFICE_HOURLY_COORDINATOR: metoffice_hourly_coordinator,
+ METOFFICE_DAILY_COORDINATOR: metoffice_daily_coordinator,
METOFFICE_NAME: site_name,
+ METOFFICE_COORDINATES: f"{latitude}_{longitude}",
}
# Fetch initial data so we have data when entities subscribe
- await metoffice_coordinator.async_refresh()
- if metoffice_data.now is None:
- raise ConfigEntryNotReady()
+ await asyncio.gather(
+ metoffice_hourly_coordinator.async_config_entry_first_refresh(),
+ metoffice_daily_coordinator.async_config_entry_first_refresh(),
+ )
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/metoffice/config_flow.py b/homeassistant/components/metoffice/config_flow.py
index 071106deff9..375eaa1ec1b 100644
--- a/homeassistant/components/metoffice/config_flow.py
+++ b/homeassistant/components/metoffice/config_flow.py
@@ -1,14 +1,15 @@
"""Config flow for Met Office integration."""
import logging
+import datapoint
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
+from homeassistant.components.metoffice.helpers import fetch_site
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
-from .data import MetOfficeData
_LOGGER = logging.getLogger(__name__)
@@ -22,12 +23,16 @@ async def validate_input(hass: core.HomeAssistant, data):
longitude = data[CONF_LONGITUDE]
api_key = data[CONF_API_KEY]
- metoffice_data = MetOfficeData(hass, api_key, latitude, longitude)
- await metoffice_data.async_update_site()
- if metoffice_data.site_name is None:
+ connection = datapoint.connection(api_key=api_key)
+
+ site = await hass.async_add_executor_job(
+ fetch_site, connection, latitude, longitude
+ )
+
+ if site is None:
raise CannotConnect()
- return {"site_name": metoffice_data.site_name}
+ return {"site_name": site.name}
class MetOfficeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py
index e710911ee59..0b275f301cd 100644
--- a/homeassistant/components/metoffice/const.py
+++ b/homeassistant/components/metoffice/const.py
@@ -25,12 +25,16 @@ ATTRIBUTION = "Data provided by the Met Office"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=15)
-METOFFICE_DATA = "metoffice_data"
-METOFFICE_COORDINATOR = "metoffice_coordinator"
+METOFFICE_COORDINATES = "metoffice_coordinates"
+METOFFICE_HOURLY_COORDINATOR = "metoffice_hourly_coordinator"
+METOFFICE_DAILY_COORDINATOR = "metoffice_daily_coordinator"
METOFFICE_MONITORED_CONDITIONS = "metoffice_monitored_conditions"
METOFFICE_NAME = "metoffice_name"
MODE_3HOURLY = "3hourly"
+MODE_3HOURLY_LABEL = "3-Hourly"
+MODE_DAILY = "daily"
+MODE_DAILY_LABEL = "Daily"
CONDITION_CLASSES = {
ATTR_CONDITION_CLOUDY: ["7", "8"],
diff --git a/homeassistant/components/metoffice/data.py b/homeassistant/components/metoffice/data.py
index 8f718b8d4b8..607c09e90b6 100644
--- a/homeassistant/components/metoffice/data.py
+++ b/homeassistant/components/metoffice/data.py
@@ -1,78 +1,11 @@
"""Common Met Office Data class used by both sensor and entity."""
-import logging
-
-import datapoint
-
-from .const import MODE_3HOURLY
-
-_LOGGER = logging.getLogger(__name__)
-
class MetOfficeData:
- """Get current and forecast data from Datapoint.
+ """Data structure for MetOffice weather and forecast."""
- Please note that the 'datapoint' library is not asyncio-friendly, so some
- calls have had to be wrapped with the standard hassio helper
- async_add_executor_job.
- """
-
- def __init__(self, hass, api_key, latitude, longitude):
+ def __init__(self, now, forecast, site):
"""Initialize the data object."""
- self._hass = hass
- self._datapoint = datapoint.connection(api_key=api_key)
- self._site = None
-
- # Public attributes
- self.latitude = latitude
- self.longitude = longitude
-
- # Holds the current data from the Met Office
- self.site_id = None
- self.site_name = None
- self.now = None
-
- async def async_update_site(self):
- """Async wrapper for getting the DataPoint site."""
- return await self._hass.async_add_executor_job(self._update_site)
-
- def _update_site(self):
- """Return the nearest DataPoint Site to the held latitude/longitude."""
- try:
- new_site = self._datapoint.get_nearest_forecast_site(
- latitude=self.latitude, longitude=self.longitude
- )
- if self._site is None or self._site.id != new_site.id:
- self._site = new_site
- self.now = None
-
- self.site_id = self._site.id
- self.site_name = self._site.name
-
- except datapoint.exceptions.APIException as err:
- _LOGGER.error("Received error from Met Office Datapoint: %s", err)
- self._site = None
- self.site_id = None
- self.site_name = None
- self.now = None
-
- return self._site
-
- async def async_update(self):
- """Async wrapper for update method."""
- return await self._hass.async_add_executor_job(self._update)
-
- def _update(self):
- """Get the latest data from DataPoint."""
- if self._site is None:
- _LOGGER.error("No Met Office forecast site held, check logs for problems")
- return
-
- try:
- forecast = self._datapoint.get_forecast_for_site(
- self._site.id, MODE_3HOURLY
- )
- self.now = forecast.now()
- except (ValueError, datapoint.exceptions.APIException) as err:
- _LOGGER.error("Check Met Office connection: %s", err.args)
- self.now = None
+ self.now = now
+ self.forecast = forecast
+ self.site = site
diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py
new file mode 100644
index 00000000000..e7518f86b5b
--- /dev/null
+++ b/homeassistant/components/metoffice/helpers.py
@@ -0,0 +1,44 @@
+"""Helpers used for Met Office integration."""
+
+import logging
+
+import datapoint
+
+from homeassistant.helpers.update_coordinator import UpdateFailed
+from homeassistant.util import utcnow
+
+from .data import MetOfficeData
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def fetch_site(connection: datapoint.Manager, latitude, longitude):
+ """Fetch site information from Datapoint API."""
+ try:
+ return connection.get_nearest_forecast_site(
+ latitude=latitude, longitude=longitude
+ )
+ except datapoint.exceptions.APIException as err:
+ _LOGGER.error("Received error from Met Office Datapoint: %s", err)
+ return None
+
+
+def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData:
+ """Fetch weather and forecast from Datapoint API."""
+ try:
+ forecast = connection.get_forecast_for_site(site.id, mode)
+ except (ValueError, datapoint.exceptions.APIException) as err:
+ _LOGGER.error("Check Met Office connection: %s", err.args)
+ raise UpdateFailed from err
+ else:
+ time_now = utcnow()
+ return MetOfficeData(
+ forecast.now(),
+ [
+ timestep
+ for day in forecast.days
+ for timestep in day.timesteps
+ if timestep.date > time_now
+ ],
+ site,
+ )
diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json
index 31a768eee8d..db6832b04b4 100644
--- a/homeassistant/components/metoffice/manifest.json
+++ b/homeassistant/components/metoffice/manifest.json
@@ -2,7 +2,7 @@
"domain": "metoffice",
"name": "Met Office",
"documentation": "https://www.home-assistant.io/integrations/metoffice",
- "requirements": ["datapoint==0.9.5"],
+ "requirements": ["datapoint==0.9.8"],
"codeowners": ["@MrHarcombe"],
"config_flow": true,
"iot_class": "cloud_polling"
diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py
index a437ecd1fea..6b45dac22e7 100644
--- a/homeassistant/components/metoffice/sensor.py
+++ b/homeassistant/components/metoffice/sensor.py
@@ -10,16 +10,21 @@ from homeassistant.const import (
TEMP_CELSIUS,
UV_INDEX,
)
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
ATTRIBUTION,
CONDITION_CLASSES,
DOMAIN,
- METOFFICE_COORDINATOR,
- METOFFICE_DATA,
+ METOFFICE_COORDINATES,
+ METOFFICE_DAILY_COORDINATOR,
+ METOFFICE_HOURLY_COORDINATOR,
METOFFICE_NAME,
+ MODE_3HOURLY_LABEL,
+ MODE_DAILY,
+ MODE_DAILY_LABEL,
VISIBILITY_CLASSES,
VISIBILITY_DISTANCE_CLASSES,
)
@@ -85,28 +90,40 @@ async def async_setup_entry(
async_add_entities(
[
- MetOfficeCurrentSensor(entry.data, hass_data, sensor_type)
+ MetOfficeCurrentSensor(
+ hass_data[METOFFICE_HOURLY_COORDINATOR], hass_data, True, sensor_type
+ )
+ for sensor_type in SENSOR_TYPES
+ ]
+ + [
+ MetOfficeCurrentSensor(
+ hass_data[METOFFICE_DAILY_COORDINATOR], hass_data, False, sensor_type
+ )
for sensor_type in SENSOR_TYPES
],
False,
)
-class MetOfficeCurrentSensor(SensorEntity):
+class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity):
"""Implementation of a Met Office current weather condition sensor."""
- def __init__(self, entry_data, hass_data, sensor_type):
+ def __init__(self, coordinator, hass_data, use_3hourly, sensor_type):
"""Initialize the sensor."""
- self._data = hass_data[METOFFICE_DATA]
- self._coordinator = hass_data[METOFFICE_COORDINATOR]
+ super().__init__(coordinator)
self._type = sensor_type
- self._name = f"{hass_data[METOFFICE_NAME]} {SENSOR_TYPES[self._type][0]}"
- self._unique_id = f"{SENSOR_TYPES[self._type][0]}_{self._data.latitude}_{self._data.longitude}"
+ mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL
+ self._name = (
+ f"{hass_data[METOFFICE_NAME]} {SENSOR_TYPES[self._type][0]} {mode_label}"
+ )
+ self._unique_id = (
+ f"{SENSOR_TYPES[self._type][0]}_{hass_data[METOFFICE_COORDINATES]}"
+ )
+ if not use_3hourly:
+ self._unique_id = f"{self._unique_id}_{MODE_DAILY}"
- self.metoffice_site_id = None
- self.metoffice_site_name = None
- self.metoffice_now = None
+ self.use_3hourly = use_3hourly
@property
def name(self):
@@ -124,22 +141,26 @@ class MetOfficeCurrentSensor(SensorEntity):
value = None
if self._type == "visibility_distance" and hasattr(
- self.metoffice_now, "visibility"
+ self.coordinator.data.now, "visibility"
):
- value = VISIBILITY_DISTANCE_CLASSES.get(self.metoffice_now.visibility.value)
+ value = VISIBILITY_DISTANCE_CLASSES.get(
+ self.coordinator.data.now.visibility.value
+ )
- if self._type == "visibility" and hasattr(self.metoffice_now, "visibility"):
- value = VISIBILITY_CLASSES.get(self.metoffice_now.visibility.value)
+ if self._type == "visibility" and hasattr(
+ self.coordinator.data.now, "visibility"
+ ):
+ value = VISIBILITY_CLASSES.get(self.coordinator.data.now.visibility.value)
- elif self._type == "weather" and hasattr(self.metoffice_now, self._type):
+ elif self._type == "weather" and hasattr(self.coordinator.data.now, self._type):
value = [
k
for k, v in CONDITION_CLASSES.items()
- if self.metoffice_now.weather.value in v
+ if self.coordinator.data.now.weather.value in v
][0]
- elif hasattr(self.metoffice_now, self._type):
- value = getattr(self.metoffice_now, self._type)
+ elif hasattr(self.coordinator.data.now, self._type):
+ value = getattr(self.coordinator.data.now, self._type)
if not isinstance(value, int):
value = value.value
@@ -175,44 +196,13 @@ class MetOfficeCurrentSensor(SensorEntity):
"""Return the state attributes of the device."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
- ATTR_LAST_UPDATE: self.metoffice_now.date if self.metoffice_now else None,
+ ATTR_LAST_UPDATE: self.coordinator.data.now.date,
ATTR_SENSOR_ID: self._type,
- ATTR_SITE_ID: self.metoffice_site_id if self.metoffice_site_id else None,
- ATTR_SITE_NAME: self.metoffice_site_name
- if self.metoffice_site_name
- else None,
+ ATTR_SITE_ID: self.coordinator.data.site.id,
+ ATTR_SITE_NAME: self.coordinator.data.site.name,
}
- async def async_added_to_hass(self) -> None:
- """Set up a listener and load data."""
- self.async_on_remove(
- self._coordinator.async_add_listener(self._update_callback)
- )
- self._update_callback()
-
- async def async_update(self):
- """Schedule a custom update via the common entity update service."""
- await self._coordinator.async_request_refresh()
-
- @callback
- def _update_callback(self) -> None:
- """Load data from integration."""
- self.metoffice_site_id = self._data.site_id
- self.metoffice_site_name = self._data.site_name
- self.metoffice_now = self._data.now
- self.async_write_ha_state()
-
- @property
- def should_poll(self) -> bool:
- """Entities do not individually poll."""
- return False
-
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
- return SENSOR_TYPES[self._type][4]
-
- @property
- def available(self):
- """Return if state is available."""
- return self.metoffice_site_id is not None and self.metoffice_now is not None
+ return SENSOR_TYPES[self._type][4] and self.use_3hourly
diff --git a/homeassistant/components/metoffice/translations/he.json b/homeassistant/components/metoffice/translations/he.json
index 4c49313d977..3ba18c5b4ab 100644
--- a/homeassistant/components/metoffice/translations/he.json
+++ b/homeassistant/components/metoffice/translations/he.json
@@ -1,8 +1,17 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
"user": {
"data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
"longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
}
}
diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py
index 5962300bb85..0b1933c665f 100644
--- a/homeassistant/components/metoffice/weather.py
+++ b/homeassistant/components/metoffice/weather.py
@@ -1,17 +1,30 @@
"""Support for UK Met Office weather service."""
-from homeassistant.components.weather import WeatherEntity
+from homeassistant.components.weather import (
+ ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TIME,
+ ATTR_FORECAST_WIND_BEARING,
+ ATTR_FORECAST_WIND_SPEED,
+ WeatherEntity,
+)
from homeassistant.const import LENGTH_KILOMETERS, TEMP_CELSIUS
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
ATTRIBUTION,
CONDITION_CLASSES,
DEFAULT_NAME,
DOMAIN,
- METOFFICE_COORDINATOR,
- METOFFICE_DATA,
+ METOFFICE_COORDINATES,
+ METOFFICE_DAILY_COORDINATOR,
+ METOFFICE_HOURLY_COORDINATOR,
METOFFICE_NAME,
+ MODE_3HOURLY_LABEL,
+ MODE_DAILY,
+ MODE_DAILY_LABEL,
VISIBILITY_CLASSES,
VISIBILITY_DISTANCE_CLASSES,
)
@@ -25,27 +38,48 @@ async def async_setup_entry(
async_add_entities(
[
- MetOfficeWeather(
- entry.data,
- hass_data,
- )
+ MetOfficeWeather(hass_data[METOFFICE_HOURLY_COORDINATOR], hass_data, True),
+ MetOfficeWeather(hass_data[METOFFICE_DAILY_COORDINATOR], hass_data, False),
],
False,
)
-class MetOfficeWeather(WeatherEntity):
+def _build_forecast_data(timestep):
+ data = {}
+ data[ATTR_FORECAST_TIME] = timestep.date
+ if timestep.weather:
+ data[ATTR_FORECAST_CONDITION] = _get_weather_condition(timestep.weather.value)
+ if timestep.precipitation:
+ data[ATTR_FORECAST_PRECIPITATION] = timestep.precipitation.value
+ if timestep.temperature:
+ data[ATTR_FORECAST_TEMP] = timestep.temperature.value
+ if timestep.wind_direction:
+ data[ATTR_FORECAST_WIND_BEARING] = timestep.wind_direction.value
+ if timestep.wind_speed:
+ data[ATTR_FORECAST_WIND_SPEED] = timestep.wind_speed.value
+ return data
+
+
+def _get_weather_condition(metoffice_code):
+ for hass_name, metoffice_codes in CONDITION_CLASSES.items():
+ if metoffice_code in metoffice_codes:
+ return hass_name
+ return None
+
+
+class MetOfficeWeather(CoordinatorEntity, WeatherEntity):
"""Implementation of a Met Office weather condition."""
- def __init__(self, entry_data, hass_data):
+ def __init__(self, coordinator, hass_data, use_3hourly):
"""Initialise the platform with a data instance."""
- self._data = hass_data[METOFFICE_DATA]
- self._coordinator = hass_data[METOFFICE_COORDINATOR]
+ super().__init__(coordinator)
- self._name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]}"
- self._unique_id = f"{self._data.latitude}_{self._data.longitude}"
-
- self.metoffice_now = None
+ mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL
+ self._name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]} {mode_label}"
+ self._unique_id = hass_data[METOFFICE_COORDINATES]
+ if not use_3hourly:
+ self._unique_id = f"{self._unique_id}_{MODE_DAILY}"
@property
def name(self):
@@ -60,24 +94,16 @@ class MetOfficeWeather(WeatherEntity):
@property
def condition(self):
"""Return the current condition."""
- return (
- [
- k
- for k, v in CONDITION_CLASSES.items()
- if self.metoffice_now.weather.value in v
- ][0]
- if self.metoffice_now
- else None
- )
+ if self.coordinator.data.now:
+ return _get_weather_condition(self.coordinator.data.now.weather.value)
+ return None
@property
def temperature(self):
"""Return the platform temperature."""
- return (
- self.metoffice_now.temperature.value
- if self.metoffice_now and self.metoffice_now.temperature
- else None
- )
+ if self.coordinator.data.now.temperature:
+ return self.coordinator.data.now.temperature.value
+ return None
@property
def temperature_unit(self):
@@ -88,8 +114,13 @@ class MetOfficeWeather(WeatherEntity):
def visibility(self):
"""Return the platform visibility."""
_visibility = None
- if hasattr(self.metoffice_now, "visibility"):
- _visibility = f"{VISIBILITY_CLASSES.get(self.metoffice_now.visibility.value)} - {VISIBILITY_DISTANCE_CLASSES.get(self.metoffice_now.visibility.value)}"
+ weather_now = self.coordinator.data.now
+ if hasattr(weather_now, "visibility"):
+ visibility_class = VISIBILITY_CLASSES.get(weather_now.visibility.value)
+ visibility_distance = VISIBILITY_DISTANCE_CLASSES.get(
+ weather_now.visibility.value
+ )
+ _visibility = f"{visibility_class} - {visibility_distance}"
return _visibility
@property
@@ -100,63 +131,46 @@ class MetOfficeWeather(WeatherEntity):
@property
def pressure(self):
"""Return the mean sea-level pressure."""
- return (
- self.metoffice_now.pressure.value
- if self.metoffice_now and self.metoffice_now.pressure
- else None
- )
+ weather_now = self.coordinator.data.now
+ if weather_now and weather_now.pressure:
+ return weather_now.pressure.value
+ return None
@property
def humidity(self):
"""Return the relative humidity."""
- return (
- self.metoffice_now.humidity.value
- if self.metoffice_now and self.metoffice_now.humidity
- else None
- )
+ weather_now = self.coordinator.data.now
+ if weather_now and weather_now.humidity:
+ return weather_now.humidity.value
+ return None
@property
def wind_speed(self):
"""Return the wind speed."""
- return (
- self.metoffice_now.wind_speed.value
- if self.metoffice_now and self.metoffice_now.wind_speed
- else None
- )
+ weather_now = self.coordinator.data.now
+ if weather_now and weather_now.wind_speed:
+ return weather_now.wind_speed.value
+ return None
@property
def wind_bearing(self):
"""Return the wind bearing."""
- return (
- self.metoffice_now.wind_direction.value
- if self.metoffice_now and self.metoffice_now.wind_direction
- else None
- )
+ weather_now = self.coordinator.data.now
+ if weather_now and weather_now.wind_direction:
+ return weather_now.wind_direction.value
+ return None
+
+ @property
+ def forecast(self):
+ """Return the forecast array."""
+ if self.coordinator.data.forecast is None:
+ return None
+ return [
+ _build_forecast_data(timestep)
+ for timestep in self.coordinator.data.forecast
+ ]
@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION
-
- async def async_added_to_hass(self) -> None:
- """Set up a listener and load data."""
- self.async_on_remove(
- self._coordinator.async_add_listener(self._update_callback)
- )
- self._update_callback()
-
- @callback
- def _update_callback(self) -> None:
- """Load data from integration."""
- self.metoffice_now = self._data.now
- self.async_write_ha_state()
-
- @property
- def should_poll(self) -> bool:
- """Entities do not individually poll."""
- return False
-
- @property
- def available(self):
- """Return if state is available."""
- return self.metoffice_now is not None
diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py
index 1e1c088b351..1c86d67ab47 100644
--- a/homeassistant/components/microsoft/tts.py
+++ b/homeassistant/components/microsoft/tts.py
@@ -20,8 +20,10 @@ _LOGGER = logging.getLogger(__name__)
SUPPORTED_LANGUAGES = [
"ar-eg",
"ar-sa",
+ "bg-bg",
"ca-es",
"cs-cz",
+ "cy-gb",
"da-dk",
"de-at",
"de-ch",
@@ -30,23 +32,42 @@ SUPPORTED_LANGUAGES = [
"en-au",
"en-ca",
"en-gb",
+ "en-hk",
"en-ie",
"en-in",
+ "en-nz",
+ "en-ph",
+ "en-sg",
"en-us",
+ "en-za",
+ "es-ar",
+ "es-co",
"es-es",
"es-mx",
+ "es-us",
+ "et-ee",
"fi-fi",
+ "fr-be",
"fr-ca",
"fr-ch",
"fr-fr",
+ "ga-ie",
+ "gu-in",
"he-il",
"hi-in",
+ "hr-hr",
"hu-hu",
"id-id",
"it-it",
"ja-jp",
"ko-kr",
+ "lt-lt",
+ "lv-lv",
+ "mr-in",
+ "ms-my",
+ "mt-mt",
"nb-no",
+ "nl-be",
"nl-nl",
"pl-pl",
"pt-br",
@@ -56,8 +77,14 @@ SUPPORTED_LANGUAGES = [
"sk-sk",
"sl-si",
"sv-se",
+ "sw-ke",
+ "ta-in",
+ "te-in",
"th-th",
"tr-tr",
+ "uk-ua",
+ "ur-pk",
+ "vi-vn",
"zh-cn",
"zh-hk",
"zh-tw",
diff --git a/homeassistant/components/mikrotik/translations/he.json b/homeassistant/components/mikrotik/translations/he.json
index ac90b3264ea..6f8286290d4 100644
--- a/homeassistant/components/mikrotik/translations/he.json
+++ b/homeassistant/components/mikrotik/translations/he.json
@@ -1,9 +1,19 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
"step": {
"user": {
"data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "name": "\u05e9\u05dd",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py
index 7a1adc6a0bc..2d591c67668 100644
--- a/homeassistant/components/mill/climate.py
+++ b/homeassistant/components/mill/climate.py
@@ -81,31 +81,26 @@ async def async_setup_entry(hass, entry, async_add_entities):
class MillHeater(ClimateEntity):
"""Representation of a Mill Thermostat device."""
+ _attr_fan_modes = [FAN_ON, HVAC_MODE_OFF]
+ _attr_max_temp = MAX_TEMP
+ _attr_min_temp = MIN_TEMP
+ _attr_supported_features = SUPPORT_FLAGS
+ _attr_target_temperature_step = 1
+ _attr_temperature_unit = TEMP_CELSIUS
+
def __init__(self, heater, mill_data_connection):
"""Initialize the thermostat."""
self._heater = heater
self._conn = mill_data_connection
- @property
- def supported_features(self):
- """Return the list of supported features."""
- return SUPPORT_FLAGS
+ self._attr_unique_id = heater.device_id
+ self._attr_name = heater.name
@property
def available(self):
"""Return True if entity is available."""
return self._heater.available
- @property
- def unique_id(self):
- """Return a unique ID."""
- return self._heater.device_id
-
- @property
- def name(self):
- """Return the name of the entity."""
- return self._heater.name
-
@property
def extra_state_attributes(self):
"""Return the state attributes."""
@@ -115,7 +110,7 @@ class MillHeater(ClimateEntity):
"controlled_by_tibber": self._heater.tibber_control,
"heater_generation": 1 if self._heater.is_gen1 else 2,
"consumption_today": self._heater.day_consumption,
- "consumption_total": self._heater.total_consumption,
+ "consumption_total": self._heater.year_consumption,
}
if self._heater.room:
res["room"] = self._heater.room.name
@@ -124,21 +119,11 @@ class MillHeater(ClimateEntity):
res["room"] = "Independent device"
return res
- @property
- def temperature_unit(self):
- """Return the unit of measurement which this thermostat uses."""
- return TEMP_CELSIUS
-
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._heater.set_temp
- @property
- def target_temperature_step(self):
- """Return the supported step of target temperature."""
- return 1
-
@property
def current_temperature(self):
"""Return the current temperature."""
@@ -149,21 +134,6 @@ class MillHeater(ClimateEntity):
"""Return the fan setting."""
return FAN_ON if self._heater.fan_status == 1 else HVAC_MODE_OFF
- @property
- def fan_modes(self):
- """List of available fan modes."""
- return [FAN_ON, HVAC_MODE_OFF]
-
- @property
- def min_temp(self):
- """Return the minimum temperature."""
- return MIN_TEMP
-
- @property
- def max_temp(self):
- """Return the maximum temperature."""
- return MAX_TEMP
-
@property
def hvac_action(self):
"""Return current hvac i.e. heat, cool, idle."""
diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json
index f41f03b66c0..161bbe274ef 100644
--- a/homeassistant/components/mill/manifest.json
+++ b/homeassistant/components/mill/manifest.json
@@ -2,7 +2,7 @@
"domain": "mill",
"name": "Mill",
"documentation": "https://www.home-assistant.io/integrations/mill",
- "requirements": ["millheater==0.4.1"],
+ "requirements": ["millheater==0.5.0"],
"codeowners": ["@danielhiversen"],
"config_flow": true,
"iot_class": "cloud_polling"
diff --git a/homeassistant/components/mill/translations/he.json b/homeassistant/components/mill/translations/he.json
new file mode 100644
index 00000000000..40170220ad7
--- /dev/null
+++ b/homeassistant/components/mill/translations/he.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/min_max/manifest.json b/homeassistant/components/min_max/manifest.json
index 525d6c0ac1a..cf8c78d46ac 100644
--- a/homeassistant/components/min_max/manifest.json
+++ b/homeassistant/components/min_max/manifest.json
@@ -4,5 +4,5 @@
"documentation": "https://www.home-assistant.io/integrations/min_max",
"codeowners": ["@fabaff"],
"quality_scale": "internal",
- "iot_class": "local_polling"
+ "iot_class": "local_push"
}
diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py
index 06fe466e8a4..a41f0018a4f 100644
--- a/homeassistant/components/minecraft_server/__init__.py
+++ b/homeassistant/components/minecraft_server/__init__.py
@@ -25,24 +25,24 @@ PLATFORMS = ["binary_sensor", "sensor"]
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Minecraft Server from a config entry."""
domain_data = hass.data.setdefault(DOMAIN, {})
# Create and store server instance.
- unique_id = config_entry.unique_id
+ unique_id = entry.unique_id
_LOGGER.debug(
"Creating server instance for '%s' (%s)",
- config_entry.data[CONF_NAME],
- config_entry.data[CONF_HOST],
+ entry.data[CONF_NAME],
+ entry.data[CONF_HOST],
)
- server = MinecraftServer(hass, unique_id, config_entry.data)
+ server = MinecraftServer(hass, unique_id, entry.data)
domain_data[unique_id] = server
await server.async_update()
server.start_periodic_update()
# Set up platforms.
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json
index 0c8df177fec..99a5ff3a463 100644
--- a/homeassistant/components/minecraft_server/manifest.json
+++ b/homeassistant/components/minecraft_server/manifest.json
@@ -3,7 +3,7 @@
"name": "Minecraft Server",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/minecraft_server",
- "requirements": ["aiodns==3.0.0", "getmac==0.8.2", "mcstatus==5.1.1"],
+ "requirements": ["aiodns==3.0.0", "getmac==0.8.2", "mcstatus==6.0.0"],
"codeowners": ["@elmurato"],
"quality_scale": "silver",
"iot_class": "local_polling"
diff --git a/homeassistant/components/minecraft_server/translations/he.json b/homeassistant/components/minecraft_server/translations/he.json
new file mode 100644
index 00000000000..4240227d571
--- /dev/null
+++ b/homeassistant/components/minecraft_server/translations/he.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05e9\u05e8\u05ea \u05e0\u05db\u05e9\u05dc\u05d4. \u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05d5\u05d0\u05ea \u05d4\u05d9\u05e6\u05d9\u05d0\u05d4 \u05d5\u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1. \u05db\u05de\u05d5 \u05db\u05df, \u05d5\u05d3\u05d0 \u05e9\u05d0\u05ea\u05d4 \u05de\u05e4\u05e2\u05d9\u05dc \u05d0\u05ea Minecraft \u05d2\u05d9\u05e8\u05e1\u05d4 1.7 \u05dc\u05e4\u05d7\u05d5\u05ea \u05d1\u05e9\u05e8\u05ea \u05e9\u05dc\u05da."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "name": "\u05e9\u05dd"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py
index e27549df169..e60bbbda78b 100644
--- a/homeassistant/components/modbus/__init__.py
+++ b/homeassistant/components/modbus/__init__.py
@@ -2,7 +2,6 @@
from __future__ import annotations
import logging
-from typing import Any
import voluptuous as vol
@@ -59,8 +58,6 @@ from .const import (
CONF_BYTESIZE,
CONF_CLIMATES,
CONF_CLOSE_COMM_ON_ERROR,
- CONF_CURRENT_TEMP,
- CONF_CURRENT_TEMP_REGISTER_TYPE,
CONF_DATA_COUNT,
CONF_DATA_TYPE,
CONF_FANS,
@@ -69,9 +66,12 @@ from .const import (
CONF_MIN_TEMP,
CONF_PARITY,
CONF_PRECISION,
- CONF_REGISTER,
+ CONF_RETRIES,
+ CONF_RETRY_ON_EMPTY,
CONF_REVERSE_ORDER,
+ CONF_RTUOVERTCP,
CONF_SCALE,
+ CONF_SERIAL,
CONF_STATE_CLOSED,
CONF_STATE_CLOSING,
CONF_STATE_OFF,
@@ -88,6 +88,8 @@ from .const import (
CONF_SWAP_WORD,
CONF_SWAP_WORD_BYTE,
CONF_TARGET_TEMP,
+ CONF_TCP,
+ CONF_UDP,
CONF_VERIFY,
CONF_WRITE_TYPE,
DATA_TYPE_CUSTOM,
@@ -97,77 +99,25 @@ from .const import (
DATA_TYPE_UINT,
DEFAULT_HUB,
DEFAULT_SCAN_INTERVAL,
- DEFAULT_STRUCTURE_PREFIX,
DEFAULT_TEMP_UNIT,
MODBUS_DOMAIN as DOMAIN,
- PLATFORMS,
)
from .modbus import async_modbus_setup
+from .validators import (
+ number_validator,
+ scan_interval_validator,
+ sensor_schema_validator,
+)
_LOGGER = logging.getLogger(__name__)
BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string})
-def number(value: Any) -> int | float:
- """Coerce a value to number without losing precision."""
- if isinstance(value, int):
- return value
- if isinstance(value, float):
- return value
-
- try:
- value = int(value)
- return value
- except (TypeError, ValueError):
- pass
- try:
- value = float(value)
- return value
- except (TypeError, ValueError) as err:
- raise vol.Invalid(f"invalid number {value}") from err
-
-
-def control_scan_interval(config: dict) -> dict:
- """Control scan_interval."""
- for hub in config:
- minimum_scan_interval = DEFAULT_SCAN_INTERVAL
- for component, conf_key in PLATFORMS:
- if conf_key not in hub:
- continue
-
- for entry in hub[conf_key]:
- scan_interval = entry.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
- if scan_interval == 0:
- continue
- if scan_interval < 5:
- _LOGGER.warning(
- "%s %s scan_interval(%d) is lower than 5 seconds, "
- "which may cause Home Assistant stability issues",
- component,
- entry.get(CONF_NAME),
- scan_interval,
- )
- entry[CONF_SCAN_INTERVAL] = scan_interval
- minimum_scan_interval = min(scan_interval, minimum_scan_interval)
- if (
- CONF_TIMEOUT in hub
- and hub[CONF_TIMEOUT] > minimum_scan_interval - 1
- and minimum_scan_interval > 1
- ):
- _LOGGER.warning(
- "Modbus %s timeout(%d) is adjusted(%d) due to scan_interval",
- hub.get(CONF_NAME, ""),
- hub[CONF_TIMEOUT],
- minimum_scan_interval - 1,
- )
- hub[CONF_TIMEOUT] = minimum_scan_interval - 1
- return config
-
-
BASE_COMPONENT_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
+ vol.Required(CONF_ADDRESS): cv.positive_int,
vol.Optional(CONF_SLAVE): cv.positive_int,
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
@@ -176,54 +126,47 @@ BASE_COMPONENT_SCHEMA = vol.Schema(
)
-CLIMATE_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
+BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
{
- vol.Required(CONF_CURRENT_TEMP): cv.positive_int,
- vol.Required(CONF_TARGET_TEMP): cv.positive_int,
- vol.Optional(CONF_DATA_COUNT, default=2): cv.positive_int,
- vol.Optional(
- CONF_CURRENT_TEMP_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING
- ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]),
- vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In(
- [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM]
+ vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
+ [
+ CALL_TYPE_REGISTER_HOLDING,
+ CALL_TYPE_REGISTER_INPUT,
+ ]
+ ),
+ vol.Optional(CONF_COUNT, default=1): cv.positive_int,
+ vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In(
+ [
+ DATA_TYPE_INT,
+ DATA_TYPE_UINT,
+ DATA_TYPE_FLOAT,
+ DATA_TYPE_STRING,
+ DATA_TYPE_CUSTOM,
+ ]
+ ),
+ vol.Optional(CONF_STRUCTURE): cv.string,
+ vol.Optional(CONF_SCALE, default=1): number_validator,
+ vol.Optional(CONF_OFFSET, default=0): number_validator,
+ vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
+ vol.Optional(CONF_SWAP, default=CONF_SWAP_NONE): vol.In(
+ [
+ CONF_SWAP_NONE,
+ CONF_SWAP_BYTE,
+ CONF_SWAP_WORD,
+ CONF_SWAP_WORD_BYTE,
+ ]
),
- vol.Optional(CONF_PRECISION, default=1): cv.positive_int,
- vol.Optional(CONF_SCALE, default=1): vol.Coerce(float),
- vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
- vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_int,
- vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int,
- vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
- vol.Optional(CONF_STRUCTURE, default=DEFAULT_STRUCTURE_PREFIX): cv.string,
- vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
}
)
-COVERS_SCHEMA = vol.All(
- cv.has_at_least_one_key(CALL_TYPE_COIL, CONF_REGISTER),
- BASE_COMPONENT_SCHEMA.extend(
- {
- vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
- vol.Optional(CONF_STATE_CLOSED, default=0): cv.positive_int,
- vol.Optional(CONF_STATE_CLOSING, default=3): cv.positive_int,
- vol.Optional(CONF_STATE_OPEN, default=1): cv.positive_int,
- vol.Optional(CONF_STATE_OPENING, default=2): cv.positive_int,
- vol.Optional(CONF_STATUS_REGISTER): cv.positive_int,
- vol.Optional(
- CONF_STATUS_REGISTER_TYPE,
- default=CALL_TYPE_REGISTER_HOLDING,
- ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]),
- vol.Exclusive(CALL_TYPE_COIL, CONF_INPUT_TYPE): cv.positive_int,
- vol.Exclusive(CONF_REGISTER, CONF_INPUT_TYPE): cv.positive_int,
- }
- ),
-)
-SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
+BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
{
- vol.Required(CONF_ADDRESS): cv.positive_int,
- vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_WRITE_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
- [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_COIL]
+ [
+ CALL_TYPE_REGISTER_HOLDING,
+ CALL_TYPE_COIL,
+ ]
),
vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int,
vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int,
@@ -246,90 +189,64 @@ SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
}
)
-LIGHT_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
- {
- vol.Required(CONF_ADDRESS): cv.positive_int,
- vol.Optional(CONF_WRITE_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
- [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_COIL]
- ),
- vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int,
- vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int,
- vol.Optional(CONF_VERIFY): vol.Maybe(
- {
- vol.Optional(CONF_ADDRESS): cv.positive_int,
- vol.Optional(CONF_INPUT_TYPE): vol.In(
- [
- CALL_TYPE_REGISTER_HOLDING,
- CALL_TYPE_DISCRETE,
- CALL_TYPE_REGISTER_INPUT,
- CALL_TYPE_COIL,
- ]
- ),
- vol.Optional(CONF_STATE_OFF): cv.positive_int,
- vol.Optional(CONF_STATE_ON): cv.positive_int,
- }
- ),
- }
+
+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,
+ vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_int,
+ vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int,
+ vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
+ vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
+ }
+ ),
)
-FAN_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
+COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
{
- vol.Required(CONF_ADDRESS): cv.positive_int,
- vol.Optional(CONF_WRITE_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
- [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_COIL]
- ),
- vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int,
- vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int,
- vol.Optional(CONF_VERIFY): vol.Maybe(
- {
- vol.Optional(CONF_ADDRESS): cv.positive_int,
- vol.Optional(CONF_INPUT_TYPE): vol.In(
- [
- CALL_TYPE_REGISTER_HOLDING,
- CALL_TYPE_DISCRETE,
- CALL_TYPE_REGISTER_INPUT,
- CALL_TYPE_COIL,
- ]
- ),
- vol.Optional(CONF_STATE_OFF): cv.positive_int,
- vol.Optional(CONF_STATE_ON): cv.positive_int,
- }
- ),
- }
-)
-
-SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
- {
- vol.Required(CONF_ADDRESS): cv.positive_int,
- vol.Optional(CONF_COUNT, default=1): cv.positive_int,
- vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In(
+ vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING,): vol.In(
[
- DATA_TYPE_INT,
- DATA_TYPE_UINT,
- DATA_TYPE_FLOAT,
- DATA_TYPE_STRING,
- DATA_TYPE_CUSTOM,
+ CALL_TYPE_REGISTER_HOLDING,
+ CALL_TYPE_COIL,
]
),
- vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
- vol.Optional(CONF_OFFSET, default=0): number,
- vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
- vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
- [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]
- ),
- vol.Optional(CONF_REVERSE_ORDER): cv.boolean,
- vol.Optional(CONF_SWAP, default=CONF_SWAP_NONE): vol.In(
- [CONF_SWAP_NONE, CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE]
- ),
- vol.Optional(CONF_SCALE, default=1): number,
- vol.Optional(CONF_STRUCTURE): cv.string,
- vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
+ vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_STATE_CLOSED, default=0): cv.positive_int,
+ vol.Optional(CONF_STATE_CLOSING, default=3): cv.positive_int,
+ vol.Optional(CONF_STATE_OPEN, default=1): cv.positive_int,
+ vol.Optional(CONF_STATE_OPENING, default=2): cv.positive_int,
+ vol.Optional(CONF_STATUS_REGISTER): cv.positive_int,
+ vol.Optional(
+ CONF_STATUS_REGISTER_TYPE,
+ default=CALL_TYPE_REGISTER_HOLDING,
+ ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]),
}
)
+SWITCH_SCHEMA = BASE_SWITCH_SCHEMA.extend(
+ {
+ vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA,
+ }
+)
+
+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_UNIT_OF_MEASUREMENT): cv.string,
+ vol.Optional(CONF_REVERSE_ORDER): cv.boolean,
+ }
+ ),
+)
+
BINARY_SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
{
- vol.Required(CONF_ADDRESS): cv.positive_int,
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In(
[CALL_TYPE_COIL, CALL_TYPE_DISCRETE]
@@ -343,13 +260,19 @@ MODBUS_SCHEMA = vol.Schema(
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
vol.Optional(CONF_CLOSE_COMM_ON_ERROR, default=True): cv.boolean,
vol.Optional(CONF_DELAY, default=0): cv.positive_int,
+ vol.Optional(CONF_RETRIES, default=3): cv.positive_int,
+ vol.Optional(CONF_RETRY_ON_EMPTY, default=False): cv.boolean,
vol.Optional(CONF_BINARY_SENSORS): vol.All(
cv.ensure_list, [BINARY_SENSOR_SCHEMA]
),
- vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATE_SCHEMA]),
+ vol.Optional(CONF_CLIMATES): vol.All(
+ cv.ensure_list, [vol.All(CLIMATE_SCHEMA, sensor_schema_validator)]
+ ),
vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]),
vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHT_SCHEMA]),
- vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]),
+ vol.Optional(CONF_SENSORS): vol.All(
+ cv.ensure_list, [vol.All(SENSOR_SCHEMA, sensor_schema_validator)]
+ ),
vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]),
vol.Optional(CONF_FANS): vol.All(cv.ensure_list, [FAN_SCHEMA]),
}
@@ -357,7 +280,7 @@ MODBUS_SCHEMA = vol.Schema(
SERIAL_SCHEMA = MODBUS_SCHEMA.extend(
{
- vol.Required(CONF_TYPE): "serial",
+ vol.Required(CONF_TYPE): CONF_SERIAL,
vol.Required(CONF_BAUDRATE): cv.positive_int,
vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8),
vol.Required(CONF_METHOD): vol.Any("rtu", "ascii"),
@@ -371,7 +294,7 @@ ETHERNET_SCHEMA = MODBUS_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port,
- vol.Required(CONF_TYPE): vol.Any("tcp", "udp", "rtuovertcp"),
+ vol.Required(CONF_TYPE): vol.Any(CONF_TCP, CONF_UDP, CONF_RTUOVERTCP),
}
)
@@ -379,7 +302,7 @@ CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
cv.ensure_list,
- control_scan_interval,
+ scan_interval_validator,
[
vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA),
],
diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py
index c27fde6d946..bc586e2f24d 100644
--- a/homeassistant/components/modbus/binary_sensor.py
+++ b/homeassistant/components/modbus/binary_sensor.py
@@ -3,70 +3,19 @@ from __future__ import annotations
import logging
-import voluptuous as vol
-
-from homeassistant.components.binary_sensor import (
- DEVICE_CLASSES_SCHEMA,
- PLATFORM_SCHEMA,
- BinarySensorEntity,
-)
-from homeassistant.const import (
- CONF_ADDRESS,
- CONF_BINARY_SENSORS,
- CONF_DEVICE_CLASS,
- CONF_NAME,
- CONF_SCAN_INTERVAL,
- CONF_SLAVE,
- STATE_ON,
-)
+from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME, STATE_ON
from homeassistant.core import HomeAssistant
-from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .base_platform import BasePlatform
-from .const import (
- CALL_TYPE_COIL,
- CALL_TYPE_DISCRETE,
- CONF_COILS,
- CONF_HUB,
- CONF_INPUT_TYPE,
- CONF_INPUTS,
- DEFAULT_HUB,
- DEFAULT_SCAN_INTERVAL,
- MODBUS_DOMAIN,
-)
+from .const import MODBUS_DOMAIN
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
-PLATFORM_SCHEMA = vol.All(
- cv.deprecated(CONF_COILS, CONF_INPUTS),
- PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_INPUTS): [
- vol.All(
- cv.deprecated(CALL_TYPE_COIL, CONF_ADDRESS),
- vol.Schema(
- {
- vol.Required(CONF_ADDRESS): cv.positive_int,
- vol.Required(CONF_NAME): cv.string,
- vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
- vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
- vol.Optional(CONF_SLAVE): cv.positive_int,
- vol.Optional(
- CONF_INPUT_TYPE, default=CALL_TYPE_COIL
- ): vol.In([CALL_TYPE_COIL, CALL_TYPE_DISCRETE]),
- }
- ),
- )
- ]
- }
- ),
-)
-
-
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
@@ -76,23 +25,11 @@ async def async_setup_platform(
"""Set up the Modbus binary sensors."""
sensors = []
- # Â check for old config:
- if discovery_info is None:
- _LOGGER.warning(
- "Binary_sensor configuration is deprecated, will be removed in a future release"
- )
- discovery_info = {
- CONF_NAME: "no name",
- CONF_BINARY_SENSORS: config[CONF_INPUTS],
- }
+ if discovery_info is None: # pragma: no cover
+ return
for entry in discovery_info[CONF_BINARY_SENSORS]:
- if CONF_HUB in entry:
- hub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]]
- else:
- hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]]
- if CONF_SCAN_INTERVAL not in entry:
- entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL
+ hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]]
sensors.append(ModbusBinarySensor(hub, entry))
async_add_entities(sensors)
diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py
index 4e6a20b1700..5c99ac86d6c 100644
--- a/homeassistant/components/modbus/climate.py
+++ b/homeassistant/components/modbus/climate.py
@@ -11,7 +11,7 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.const import (
- CONF_ADDRESS,
+ CONF_COUNT,
CONF_NAME,
CONF_OFFSET,
CONF_STRUCTURE,
@@ -29,19 +29,17 @@ from .const import (
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_WRITE_REGISTERS,
CONF_CLIMATES,
- CONF_CURRENT_TEMP,
- CONF_CURRENT_TEMP_REGISTER_TYPE,
- CONF_DATA_COUNT,
CONF_DATA_TYPE,
- CONF_INPUT_TYPE,
CONF_MAX_TEMP,
CONF_MIN_TEMP,
CONF_PRECISION,
CONF_SCALE,
CONF_STEP,
+ CONF_SWAP,
+ CONF_SWAP_BYTE,
+ CONF_SWAP_WORD,
+ CONF_SWAP_WORD_BYTE,
CONF_TARGET_TEMP,
- DATA_TYPE_CUSTOM,
- DEFAULT_STRUCT_FORMAT,
MODBUS_DOMAIN,
)
from .modbus import ModbusHub
@@ -63,37 +61,6 @@ async def async_setup_platform(
entities = []
for entity in discovery_info[CONF_CLIMATES]:
hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]]
- count = entity[CONF_DATA_COUNT]
- data_type = entity[CONF_DATA_TYPE]
- name = entity[CONF_NAME]
- structure = entity[CONF_STRUCTURE]
-
- if data_type != DATA_TYPE_CUSTOM:
- try:
- structure = f">{DEFAULT_STRUCT_FORMAT[data_type][count]}"
- except KeyError:
- _LOGGER.error(
- "Climate %s: Unable to find a data type matching count value %s, try a custom type",
- name,
- count,
- )
- continue
-
- try:
- size = struct.calcsize(structure)
- except struct.error as err:
- _LOGGER.error("Error in sensor %s structure: %s", name, err)
- continue
-
- if count * 2 != size:
- _LOGGER.error(
- "Structure size (%d bytes) mismatch registers count (%d words)",
- size,
- count,
- )
- continue
-
- entity[CONF_STRUCTURE] = structure
entities.append(ModbusThermostat(hub, entity))
async_add_entities(entities)
@@ -108,19 +75,13 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity):
config: dict[str, Any],
) -> None:
"""Initialize the modbus thermostat."""
- config[CONF_ADDRESS] = "0"
- config[CONF_INPUT_TYPE] = ""
super().__init__(hub, config)
self._target_temperature_register = config[CONF_TARGET_TEMP]
- self._current_temperature_register = config[CONF_CURRENT_TEMP]
- self._current_temperature_register_type = config[
- CONF_CURRENT_TEMP_REGISTER_TYPE
- ]
self._target_temperature = None
self._current_temperature = None
self._data_type = config[CONF_DATA_TYPE]
self._structure = config[CONF_STRUCTURE]
- self._count = config[CONF_DATA_COUNT]
+ self._count = config[CONF_COUNT]
self._precision = config[CONF_PRECISION]
self._scale = config[CONF_SCALE]
self._offset = config[CONF_OFFSET]
@@ -128,6 +89,7 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity):
self._max_temp = config[CONF_MAX_TEMP]
self._min_temp = config[CONF_MIN_TEMP]
self._temp_step = config[CONF_STEP]
+ self._swap = config[CONF_SWAP]
async def async_added_to_hass(self):
"""Handle entity which will be added."""
@@ -204,6 +166,21 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity):
self._available = result is not None
await self.async_update()
+ def _swap_registers(self, registers):
+ """Do swap as needed."""
+ if self._swap in [CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE]:
+ # convert [12][34] --> [21][43]
+ for i, register in enumerate(registers):
+ registers[i] = int.from_bytes(
+ register.to_bytes(2, byteorder="little"),
+ byteorder="big",
+ signed=False,
+ )
+ if self._swap in [CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE]:
+ # convert [12][34] ==> [34][12]
+ registers.reverse()
+ return registers
+
async def async_update(self, now=None):
"""Update Target & Current Temperature."""
# remark "now" is a dummy parameter to avoid problems with
@@ -212,7 +189,7 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity):
CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register
)
self._current_temperature = await self._async_read_register(
- self._current_temperature_register_type, self._current_temperature_register
+ self._input_type, self._address
)
self.async_write_ha_state()
@@ -226,9 +203,8 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity):
self._available = False
return -1
- byte_string = b"".join(
- [x.to_bytes(2, byteorder="big") for x in result.registers]
- )
+ registers = self._swap_registers(result.registers)
+ byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers])
val = struct.unpack(self._structure, byte_string)
if len(val) != 1 or not isinstance(val[0], (float, int)):
_LOGGER.error(
diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py
index cfda4a3863a..8fb4626d2fe 100644
--- a/homeassistant/components/modbus/const.py
+++ b/homeassistant/components/modbus/const.py
@@ -35,9 +35,13 @@ CONF_PARITY = "parity"
CONF_REGISTER = "register"
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_RTUOVERTCP = "rtuovertcp"
CONF_SCALE = "scale"
+CONF_SERIAL = "serial"
CONF_STATE_CLOSED = "state_closed"
CONF_STATE_CLOSING = "state_closing"
CONF_STATE_OFF = "state_off"
@@ -54,6 +58,8 @@ CONF_SWAP_NONE = "none"
CONF_SWAP_WORD = "word"
CONF_SWAP_WORD_BYTE = "word_byte"
CONF_TARGET_TEMP = "target_temp_register"
+CONF_TCP = "tcp"
+CONF_UDP = "udp"
CONF_VERIFY = "verify"
CONF_VERIFY_REGISTER = "verify_register"
CONF_VERIFY_STATE = "verify_state"
diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py
index ca00576770e..88c8fd77ae8 100644
--- a/homeassistant/components/modbus/cover.py
+++ b/homeassistant/components/modbus/cover.py
@@ -6,7 +6,6 @@ from typing import Any
from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity
from homeassistant.const import (
- CONF_ADDRESS,
CONF_COVERS,
CONF_NAME,
STATE_CLOSED,
@@ -23,11 +22,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .base_platform import BasePlatform
from .const import (
CALL_TYPE_COIL,
- CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_WRITE_COIL,
CALL_TYPE_WRITE_REGISTER,
- CONF_INPUT_TYPE,
- CONF_REGISTER,
CONF_STATE_CLOSED,
CONF_STATE_CLOSING,
CONF_STATE_OPEN,
@@ -49,12 +45,7 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
):
"""Read configuration and create Modbus cover."""
- if discovery_info is None:
- _LOGGER.warning(
- "You're trying to init Modbus Cover in an unsupported way."
- " Check https://www.home-assistant.io/integrations/modbus/#configuring-platform-cover"
- " and fix your configuration"
- )
+ if discovery_info is None: # pragma: no cover
return
covers = []
@@ -74,11 +65,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity):
config: dict[str, Any],
) -> None:
"""Initialize the modbus cover."""
- config[CONF_ADDRESS] = "0"
- config[CONF_INPUT_TYPE] = ""
super().__init__(hub, config)
- self._coil = config.get(CALL_TYPE_COIL)
- self._register = config.get(CONF_REGISTER)
self._state_closed = config[CONF_STATE_CLOSED]
self._state_closing = config[CONF_STATE_CLOSING]
self._state_open = config[CONF_STATE_OPEN]
@@ -89,22 +76,23 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity):
# If we read cover status from coil, and not from optional status register,
# we interpret boolean value False as closed cover, and value True as open cover.
# Intermediate states are not supported in such a setup.
- if self._coil is not None:
+ if self._input_type == CALL_TYPE_COIL:
self._write_type = CALL_TYPE_WRITE_COIL
+ self._write_address = self._address
if self._status_register is None:
self._state_closed = False
self._state_open = True
self._state_closing = None
self._state_opening = None
-
- # If we read cover status from the main register (i.e., an optional
- # status register is not specified), we need to make sure the register_type
- # is set to "holding".
- if self._register is not None:
+ else:
+ # If we read cover status from the main register (i.e., an optional
+ # status register is not specified), we need to make sure the register_type
+ # is set to "holding".
self._write_type = CALL_TYPE_WRITE_REGISTER
- if self._status_register is None:
- self._status_register = self._register
- self._status_register_type = CALL_TYPE_REGISTER_HOLDING
+ self._write_address = self._address
+ if self._status_register:
+ self._address = self._status_register
+ self._input_type = self._status_register_type
async def async_added_to_hass(self):
"""Handle entity which will be added."""
@@ -144,7 +132,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity):
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open cover."""
result = await self._hub.async_pymodbus_call(
- self._slave, self._register, self._state_open, self._write_type
+ self._slave, self._write_address, self._state_open, self._write_type
)
self._available = result is not None
await self.async_update()
@@ -152,7 +140,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity):
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
result = await self._hub.async_pymodbus_call(
- self._slave, self._register, self._state_closed, self._write_type
+ self._slave, self._write_address, self._state_closed, self._write_type
)
self._available = result is not None
await self.async_update()
@@ -161,35 +149,16 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity):
"""Update the state of the cover."""
# remark "now" is a dummy parameter to avoid problems with
# async_track_time_interval
- if self._coil is not None and self._status_register is None:
- self._value = await self._async_read_coil()
- else:
- self._value = await self._async_read_status_register()
-
- self.async_write_ha_state()
-
- async def _async_read_status_register(self) -> int | None:
- """Read status register using the Modbus hub slave."""
result = await self._hub.async_pymodbus_call(
- self._slave, self._status_register, 1, self._status_register_type
+ self._slave, self._address, 1, self._input_type
)
if result is None:
self._available = False
+ self.async_write_ha_state()
return None
-
- value = int(result.registers[0])
self._available = True
-
- return value
-
- async def _async_read_coil(self) -> bool | None:
- """Read coil using the Modbus hub slave."""
- result = await self._hub.async_pymodbus_call(
- self._slave, self._coil, 1, CALL_TYPE_COIL
- )
- if result is None:
- self._available = False
- return None
-
- value = bool(result.bits[0] & 1)
- return value
+ if self._input_type == CALL_TYPE_COIL:
+ self._value = bool(result.bits[0] & 1)
+ else:
+ self._value = int(result.registers[0])
+ self.async_write_ha_state()
diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py
index 7fabff25711..a4d4265846d 100644
--- a/homeassistant/components/modbus/fan.py
+++ b/homeassistant/components/modbus/fan.py
@@ -20,7 +20,7 @@ async def async_setup_platform(
hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
):
"""Read configuration and create Modbus fans."""
- if discovery_info is None:
+ if discovery_info is None: # pragma: no cover
return
fans = []
diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py
index f56b01ff001..3eae5ed3db3 100644
--- a/homeassistant/components/modbus/light.py
+++ b/homeassistant/components/modbus/light.py
@@ -20,8 +20,9 @@ async def async_setup_platform(
hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
):
"""Read configuration and create Modbus lights."""
- if discovery_info is None:
+ if discovery_info is None: # pragma: no cover
return
+
lights = []
for entry in discovery_info[CONF_LIGHTS]:
hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]]
diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py
index 4a02f019238..2e5892dbf1d 100644
--- a/homeassistant/components/modbus/modbus.py
+++ b/homeassistant/components/modbus/modbus.py
@@ -39,7 +39,13 @@ from .const import (
CONF_BYTESIZE,
CONF_CLOSE_COMM_ON_ERROR,
CONF_PARITY,
+ CONF_RETRIES,
+ CONF_RETRY_ON_EMPTY,
+ CONF_RTUOVERTCP,
+ CONF_SERIAL,
CONF_STOPBITS,
+ CONF_TCP,
+ CONF_UDP,
DEFAULT_HUB,
MODBUS_DOMAIN as DOMAIN,
PLATFORMS,
@@ -49,9 +55,53 @@ from .const import (
ENTRY_FUNC = "func"
ENTRY_ATTR = "attr"
+ENTRY_NAME = "name"
_LOGGER = logging.getLogger(__name__)
+PYMODBUS_CALL = {
+ CALL_TYPE_COIL: {
+ ENTRY_ATTR: "bits",
+ ENTRY_NAME: "read_coils",
+ ENTRY_FUNC: None,
+ },
+ CALL_TYPE_DISCRETE: {
+ ENTRY_ATTR: "bits",
+ ENTRY_NAME: "read_discrete_inputs",
+ ENTRY_FUNC: None,
+ },
+ CALL_TYPE_REGISTER_HOLDING: {
+ ENTRY_ATTR: "registers",
+ ENTRY_NAME: "read_holding_registers",
+ ENTRY_FUNC: None,
+ },
+ CALL_TYPE_REGISTER_INPUT: {
+ ENTRY_ATTR: "registers",
+ ENTRY_NAME: "read_input_registers",
+ ENTRY_FUNC: None,
+ },
+ CALL_TYPE_WRITE_COIL: {
+ ENTRY_ATTR: "value",
+ ENTRY_NAME: "write_coil",
+ ENTRY_FUNC: None,
+ },
+ CALL_TYPE_WRITE_COILS: {
+ ENTRY_ATTR: "count",
+ ENTRY_NAME: "write_coils",
+ ENTRY_FUNC: None,
+ },
+ CALL_TYPE_WRITE_REGISTER: {
+ ENTRY_ATTR: "value",
+ ENTRY_NAME: "write_register",
+ ENTRY_FUNC: None,
+ },
+ CALL_TYPE_WRITE_REGISTERS: {
+ ENTRY_ATTR: "count",
+ ENTRY_NAME: "write_registers",
+ ENTRY_FUNC: None,
+ },
+}
+
async def async_modbus_setup(
hass, config, service_write_register_schema, service_write_coil_schema
@@ -65,7 +115,8 @@ async def async_modbus_setup(
# modbus needs to be activated before components are loaded
# to avoid a racing problem
- await my_hub.async_setup()
+ if not await my_hub.async_setup():
+ return False
# load platforms
for component, conf_key in PLATFORMS:
@@ -144,59 +195,42 @@ class ModbusHub:
self.hass = hass
self._config_name = client_config[CONF_NAME]
self._config_type = client_config[CONF_TYPE]
- self._config_port = client_config[CONF_PORT]
- self._config_timeout = client_config[CONF_TIMEOUT]
self._config_delay = client_config[CONF_DELAY]
- self._config_reset_socket = client_config[CONF_CLOSE_COMM_ON_ERROR]
- Defaults.Timeout = client_config[CONF_TIMEOUT]
- if self._config_type == "serial":
+ self._pb_call = PYMODBUS_CALL.copy()
+ self._pb_class = {
+ CONF_SERIAL: ModbusSerialClient,
+ CONF_TCP: ModbusTcpClient,
+ CONF_UDP: ModbusUdpClient,
+ CONF_RTUOVERTCP: ModbusTcpClient,
+ }
+ self._pb_params = {
+ "port": client_config[CONF_PORT],
+ "timeout": client_config[CONF_TIMEOUT],
+ "reset_socket": client_config[CONF_CLOSE_COMM_ON_ERROR],
+ "retries": client_config[CONF_RETRIES],
+ "retry_on_empty": client_config[CONF_RETRY_ON_EMPTY],
+ }
+ if self._config_type == CONF_SERIAL:
# serial configuration
- self._config_method = client_config[CONF_METHOD]
- self._config_baudrate = client_config[CONF_BAUDRATE]
- self._config_stopbits = client_config[CONF_STOPBITS]
- self._config_bytesize = client_config[CONF_BYTESIZE]
- self._config_parity = client_config[CONF_PARITY]
+ self._pb_params.update(
+ {
+ "method": client_config[CONF_METHOD],
+ "baudrate": client_config[CONF_BAUDRATE],
+ "stopbits": client_config[CONF_STOPBITS],
+ "bytesize": client_config[CONF_BYTESIZE],
+ "parity": client_config[CONF_PARITY],
+ }
+ )
else:
# network configuration
- self._config_host = client_config[CONF_HOST]
+ self._pb_params["host"] = client_config[CONF_HOST]
+ if self._config_type == CONF_RTUOVERTCP:
+ self._pb_params["framer"] = ModbusRtuFramer
- self._call_type = {
- CALL_TYPE_COIL: {
- ENTRY_ATTR: "bits",
- ENTRY_FUNC: None,
- },
- CALL_TYPE_DISCRETE: {
- ENTRY_ATTR: "bits",
- ENTRY_FUNC: None,
- },
- CALL_TYPE_REGISTER_HOLDING: {
- ENTRY_ATTR: "registers",
- ENTRY_FUNC: None,
- },
- CALL_TYPE_REGISTER_INPUT: {
- ENTRY_ATTR: "registers",
- ENTRY_FUNC: None,
- },
- CALL_TYPE_WRITE_COIL: {
- ENTRY_ATTR: "value",
- ENTRY_FUNC: None,
- },
- CALL_TYPE_WRITE_COILS: {
- ENTRY_ATTR: "count",
- ENTRY_FUNC: None,
- },
- CALL_TYPE_WRITE_REGISTER: {
- ENTRY_ATTR: "value",
- ENTRY_FUNC: None,
- },
- CALL_TYPE_WRITE_REGISTERS: {
- ENTRY_ATTR: "count",
- ENTRY_FUNC: None,
- },
- }
+ Defaults.Timeout = client_config[CONF_TIMEOUT]
- def _log_error(self, exception_error: ModbusException, error_state=True):
- log_text = "Pymodbus: " + str(exception_error)
+ def _log_error(self, text: str, error_state=True):
+ log_text = f"Pymodbus: {text}"
if self._in_error:
_LOGGER.debug(log_text)
else:
@@ -206,71 +240,25 @@ class ModbusHub:
async def async_setup(self):
"""Set up pymodbus client."""
try:
- if self._config_type == "serial":
- self._client = ModbusSerialClient(
- method=self._config_method,
- port=self._config_port,
- baudrate=self._config_baudrate,
- stopbits=self._config_stopbits,
- bytesize=self._config_bytesize,
- parity=self._config_parity,
- timeout=self._config_timeout,
- retry_on_empty=True,
- reset_socket=self._config_reset_socket,
- )
- elif self._config_type == "rtuovertcp":
- self._client = ModbusTcpClient(
- host=self._config_host,
- port=self._config_port,
- framer=ModbusRtuFramer,
- timeout=self._config_timeout,
- reset_socket=self._config_reset_socket,
- )
- elif self._config_type == "tcp":
- self._client = ModbusTcpClient(
- host=self._config_host,
- port=self._config_port,
- timeout=self._config_timeout,
- reset_socket=self._config_reset_socket,
- )
- elif self._config_type == "udp":
- self._client = ModbusUdpClient(
- host=self._config_host,
- port=self._config_port,
- timeout=self._config_timeout,
- reset_socket=self._config_reset_socket,
- )
+ self._client = self._pb_class[self._config_type](**self._pb_params)
except ModbusException as exception_error:
- self._log_error(exception_error, error_state=False)
- return
+ self._log_error(str(exception_error), error_state=False)
+ return False
+
+ for entry in self._pb_call.values():
+ entry[ENTRY_FUNC] = getattr(self._client, entry[ENTRY_NAME])
async with self._lock:
- await self.hass.async_add_executor_job(self._pymodbus_connect)
-
- self._call_type[CALL_TYPE_COIL][ENTRY_FUNC] = self._client.read_coils
- self._call_type[CALL_TYPE_DISCRETE][
- ENTRY_FUNC
- ] = self._client.read_discrete_inputs
- self._call_type[CALL_TYPE_REGISTER_HOLDING][
- ENTRY_FUNC
- ] = self._client.read_holding_registers
- self._call_type[CALL_TYPE_REGISTER_INPUT][
- ENTRY_FUNC
- ] = self._client.read_input_registers
- self._call_type[CALL_TYPE_WRITE_COIL][ENTRY_FUNC] = self._client.write_coil
- self._call_type[CALL_TYPE_WRITE_COILS][ENTRY_FUNC] = self._client.write_coils
- self._call_type[CALL_TYPE_WRITE_REGISTER][
- ENTRY_FUNC
- ] = self._client.write_register
- self._call_type[CALL_TYPE_WRITE_REGISTERS][
- ENTRY_FUNC
- ] = self._client.write_registers
+ if not await self.hass.async_add_executor_job(self._pymodbus_connect):
+ self._log_error("initial connect failed, no retry", error_state=False)
+ return False
# Start counting down to allow modbus requests.
if self._config_delay:
self._async_cancel_listener = async_call_later(
self.hass, self._config_delay, self.async_end_delay
)
+ return True
@callback
def async_end_delay(self, args):
@@ -284,7 +272,7 @@ class ModbusHub:
try:
self._client.close()
except ModbusException as exception_error:
- self._log_error(exception_error)
+ self._log_error(str(exception_error))
self._client = None
async def async_close(self):
@@ -299,20 +287,21 @@ class ModbusHub:
def _pymodbus_connect(self):
"""Connect client."""
try:
- self._client.connect()
+ return self._client.connect()
except ModbusException as exception_error:
- self._log_error(exception_error, error_state=False)
+ self._log_error(str(exception_error), error_state=False)
+ return False
def _pymodbus_call(self, unit, address, value, use_call):
"""Call sync. pymodbus."""
kwargs = {"unit": unit} if unit else {}
try:
- result = self._call_type[use_call][ENTRY_FUNC](address, value, **kwargs)
+ result = self._pb_call[use_call][ENTRY_FUNC](address, value, **kwargs)
except ModbusException as exception_error:
- self._log_error(exception_error)
- result = exception_error
- if not hasattr(result, self._call_type[use_call][ENTRY_ATTR]):
- self._log_error(result)
+ self._log_error(str(exception_error))
+ return None
+ if not hasattr(result, self._pb_call[use_call][ENTRY_ATTR]):
+ self._log_error(str(result))
return None
self._in_error = False
return result
@@ -321,7 +310,13 @@ class ModbusHub:
"""Convert async to sync pymodbus call."""
if self._config_delay:
return None
+ if not self._client.is_socket_open():
+ return None
async with self._lock:
- return await self.hass.async_add_executor_job(
+ result = await self.hass.async_add_executor_job(
self._pymodbus_call, unit, address, value, use_call
)
+ if self._config_type == "serial":
+ # small delay until next request/response
+ await asyncio.sleep(30 / 1000)
+ return result
diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py
index 85bb591711c..9f1e7572a58 100644
--- a/homeassistant/components/modbus/sensor.py
+++ b/homeassistant/components/modbus/sensor.py
@@ -3,99 +3,39 @@ from __future__ import annotations
import logging
import struct
+from typing import Any
-import voluptuous as vol
-
-from homeassistant.components.sensor import (
- DEVICE_CLASSES_SCHEMA,
- PLATFORM_SCHEMA,
- SensorEntity,
-)
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
- CONF_ADDRESS,
CONF_COUNT,
- CONF_DEVICE_CLASS,
CONF_NAME,
CONF_OFFSET,
- CONF_SCAN_INTERVAL,
CONF_SENSORS,
- CONF_SLAVE,
CONF_STRUCTURE,
CONF_UNIT_OF_MEASUREMENT,
)
from homeassistant.core import HomeAssistant
-from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
-from . import number
from .base_platform import BasePlatform
from .const import (
- CALL_TYPE_REGISTER_HOLDING,
- CALL_TYPE_REGISTER_INPUT,
CONF_DATA_TYPE,
- CONF_HUB,
- CONF_INPUT_TYPE,
CONF_PRECISION,
- CONF_REGISTER,
- CONF_REGISTER_TYPE,
- CONF_REGISTERS,
- CONF_REVERSE_ORDER,
CONF_SCALE,
CONF_SWAP,
CONF_SWAP_BYTE,
- CONF_SWAP_NONE,
CONF_SWAP_WORD,
CONF_SWAP_WORD_BYTE,
- DATA_TYPE_CUSTOM,
- DATA_TYPE_FLOAT,
- DATA_TYPE_INT,
DATA_TYPE_STRING,
- DATA_TYPE_UINT,
- DEFAULT_HUB,
- DEFAULT_SCAN_INTERVAL,
- DEFAULT_STRUCT_FORMAT,
MODBUS_DOMAIN,
)
+from .modbus import ModbusHub
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_REGISTERS): [
- {
- vol.Required(CONF_NAME): cv.string,
- vol.Required(CONF_REGISTER): cv.positive_int,
- vol.Optional(CONF_COUNT, default=1): cv.positive_int,
- vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In(
- [
- DATA_TYPE_INT,
- DATA_TYPE_UINT,
- DATA_TYPE_FLOAT,
- DATA_TYPE_STRING,
- DATA_TYPE_CUSTOM,
- ]
- ),
- vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
- vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
- vol.Optional(CONF_OFFSET, default=0): number,
- vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
- vol.Optional(
- CONF_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING
- ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]),
- vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean,
- vol.Optional(CONF_SCALE, default=1): number,
- vol.Optional(CONF_SLAVE): cv.positive_int,
- vol.Optional(CONF_STRUCTURE): cv.string,
- vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
- }
- ]
- }
-)
-
-
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
@@ -105,91 +45,13 @@ async def async_setup_platform(
"""Set up the Modbus sensors."""
sensors = []
- # Â check for old config:
- if discovery_info is None:
- _LOGGER.warning(
- "Sensor configuration is deprecated, will be removed in a future release"
- )
- discovery_info = {
- CONF_NAME: "no name",
- CONF_SENSORS: config[CONF_REGISTERS],
- }
- for entry in discovery_info[CONF_SENSORS]:
- entry[CONF_ADDRESS] = entry[CONF_REGISTER]
- entry[CONF_INPUT_TYPE] = entry[CONF_REGISTER_TYPE]
- del entry[CONF_REGISTER]
- del entry[CONF_REGISTER_TYPE]
+ if discovery_info is None: # pragma: no cover
+ return
for entry in discovery_info[CONF_SENSORS]:
- if entry[CONF_DATA_TYPE] == DATA_TYPE_STRING:
- structure = str(entry[CONF_COUNT] * 2) + "s"
- elif entry[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM:
- try:
- structure = f">{DEFAULT_STRUCT_FORMAT[entry[CONF_DATA_TYPE]][entry[CONF_COUNT]]}"
- except KeyError:
- _LOGGER.error(
- "Unable to detect data type for %s sensor, try a custom type",
- entry[CONF_NAME],
- )
- continue
- else:
- structure = entry.get(CONF_STRUCTURE)
+ hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]]
+ sensors.append(ModbusRegisterSensor(hub, entry))
- try:
- size = struct.calcsize(structure)
- except struct.error as err:
- _LOGGER.error("Error in sensor %s structure: %s", entry[CONF_NAME], err)
- continue
-
- bytecount = entry[CONF_COUNT] * 2
- if bytecount != size:
- _LOGGER.error(
- "Structure request %d bytes, but %d registers have a size of %d bytes",
- size,
- entry[CONF_COUNT],
- bytecount,
- )
- continue
-
- if CONF_REVERSE_ORDER in entry:
- if entry[CONF_REVERSE_ORDER]:
- entry[CONF_SWAP] = CONF_SWAP_WORD
- else:
- entry[CONF_SWAP] = CONF_SWAP_NONE
- del entry[CONF_REVERSE_ORDER]
- if entry.get(CONF_SWAP) != CONF_SWAP_NONE:
- if entry[CONF_SWAP] == CONF_SWAP_BYTE:
- regs_needed = 1
- else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD
- regs_needed = 2
- if (
- entry[CONF_COUNT] < regs_needed
- or (entry[CONF_COUNT] % regs_needed) != 0
- ):
- _LOGGER.error(
- "Error in sensor %s swap(%s) not possible due to count: %d",
- entry[CONF_NAME],
- entry[CONF_SWAP],
- entry[CONF_COUNT],
- )
- continue
- if CONF_HUB in entry:
- # from old config!
- hub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]]
- else:
- hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]]
- if CONF_SCAN_INTERVAL not in entry:
- entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL
- sensors.append(
- ModbusRegisterSensor(
- hub,
- entry,
- structure,
- )
- )
-
- if not sensors:
- return
async_add_entities(sensors)
@@ -198,21 +60,18 @@ class ModbusRegisterSensor(BasePlatform, RestoreEntity, SensorEntity):
def __init__(
self,
- hub,
- entry,
- structure,
- ):
+ hub: ModbusHub,
+ entry: dict[str, Any],
+ ) -> None:
"""Initialize the modbus register sensor."""
super().__init__(hub, entry)
- self._register = self._address
- self._register_type = self._input_type
self._unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT)
self._count = int(entry[CONF_COUNT])
self._swap = entry[CONF_SWAP]
self._scale = entry[CONF_SCALE]
self._offset = entry[CONF_OFFSET]
self._precision = entry[CONF_PRECISION]
- self._structure = structure
+ self._structure = entry.get(CONF_STRUCTURE)
self._data_type = entry[CONF_DATA_TYPE]
async def async_added_to_hass(self):
@@ -252,7 +111,7 @@ class ModbusRegisterSensor(BasePlatform, RestoreEntity, SensorEntity):
# remark "now" is a dummy parameter to avoid problems with
# async_track_time_interval
result = await self._hub.async_pymodbus_call(
- self._slave, self._register, self._count, self._register_type
+ self._slave, self._address, self._count, self._input_type
)
if result is None:
self._available = False
diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py
index 98e15d5b311..820e43419a0 100644
--- a/homeassistant/components/modbus/switch.py
+++ b/homeassistant/components/modbus/switch.py
@@ -22,6 +22,9 @@ async def async_setup_platform(
"""Read configuration and create Modbus switches."""
switches = []
+ if discovery_info is None: # pragma: no cover
+ return
+
for entry in discovery_info[CONF_SWITCHES]:
hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]]
switches.append(ModbusSwitch(hub, entry))
diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py
new file mode 100644
index 00000000000..03f27dd461b
--- /dev/null
+++ b/homeassistant/components/modbus/validators.py
@@ -0,0 +1,144 @@
+"""Validate Modbus configuration."""
+from __future__ import annotations
+
+import logging
+import struct
+from typing import Any
+
+import voluptuous as vol
+
+from homeassistant.const import (
+ CONF_COUNT,
+ CONF_NAME,
+ CONF_SCAN_INTERVAL,
+ CONF_STRUCTURE,
+ CONF_TIMEOUT,
+)
+
+from .const import (
+ CONF_DATA_TYPE,
+ CONF_SWAP,
+ CONF_SWAP_BYTE,
+ CONF_SWAP_NONE,
+ DATA_TYPE_CUSTOM,
+ DATA_TYPE_STRING,
+ DEFAULT_SCAN_INTERVAL,
+ DEFAULT_STRUCT_FORMAT,
+ PLATFORMS,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def sensor_schema_validator(config):
+ """Sensor schema validator."""
+
+ if config[CONF_DATA_TYPE] == DATA_TYPE_STRING:
+ structure = str(config[CONF_COUNT] * 2) + "s"
+ elif config[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM:
+ try:
+ structure = (
+ f">{DEFAULT_STRUCT_FORMAT[config[CONF_DATA_TYPE]][config[CONF_COUNT]]}"
+ )
+ except KeyError as key:
+ raise vol.Invalid(
+ f"Unable to detect data type for {config[CONF_NAME]} sensor, try a custom type"
+ ) from key
+ else:
+ structure = config.get(CONF_STRUCTURE)
+
+ if not structure:
+ raise vol.Invalid(
+ f"Error in sensor {config[CONF_NAME]}. The `{CONF_STRUCTURE}` field can not be empty "
+ f"if the parameter `{CONF_DATA_TYPE}` is set to the `{DATA_TYPE_CUSTOM}`"
+ )
+
+ try:
+ size = struct.calcsize(structure)
+ except struct.error as err:
+ raise vol.Invalid(
+ f"Error in sensor {config[CONF_NAME]} structure: {str(err)}"
+ ) from err
+
+ bytecount = config[CONF_COUNT] * 2
+ if bytecount != size:
+ raise vol.Invalid(
+ f"Structure request {size} bytes, "
+ f"but {config[CONF_COUNT]} registers have a size of {bytecount} bytes"
+ )
+
+ swap_type = config.get(CONF_SWAP)
+
+ if config.get(CONF_SWAP) != CONF_SWAP_NONE:
+ if swap_type == CONF_SWAP_BYTE:
+ regs_needed = 1
+ else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD
+ regs_needed = 2
+ if config[CONF_COUNT] < regs_needed or (config[CONF_COUNT] % regs_needed) != 0:
+ raise vol.Invalid(
+ f"Error in sensor {config[CONF_NAME]} swap({swap_type}) "
+ f"not possible due to the registers "
+ f"count: {config[CONF_COUNT]}, needed: {regs_needed}"
+ )
+
+ return {
+ **config,
+ CONF_STRUCTURE: structure,
+ CONF_SWAP: swap_type,
+ }
+
+
+def number_validator(value: Any) -> int | float:
+ """Coerce a value to number without losing precision."""
+ if isinstance(value, int):
+ return value
+ if isinstance(value, float):
+ return value
+
+ try:
+ value = int(value)
+ return value
+ except (TypeError, ValueError):
+ pass
+ try:
+ value = float(value)
+ return value
+ except (TypeError, ValueError) as err:
+ raise vol.Invalid(f"invalid number {value}") from err
+
+
+def scan_interval_validator(config: dict) -> dict:
+ """Control scan_interval."""
+ for hub in config:
+ minimum_scan_interval = DEFAULT_SCAN_INTERVAL
+ for component, conf_key in PLATFORMS:
+ if conf_key not in hub:
+ continue
+
+ for entry in hub[conf_key]:
+ scan_interval = entry.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+ if scan_interval == 0:
+ continue
+ if scan_interval < 5:
+ _LOGGER.warning(
+ "%s %s scan_interval(%d) is lower than 5 seconds, "
+ "which may cause Home Assistant stability issues",
+ component,
+ entry.get(CONF_NAME),
+ scan_interval,
+ )
+ entry[CONF_SCAN_INTERVAL] = scan_interval
+ minimum_scan_interval = min(scan_interval, minimum_scan_interval)
+ if (
+ CONF_TIMEOUT in hub
+ and hub[CONF_TIMEOUT] > minimum_scan_interval - 1
+ and minimum_scan_interval > 1
+ ):
+ _LOGGER.warning(
+ "Modbus %s timeout(%d) is adjusted(%d) due to scan_interval",
+ hub.get(CONF_NAME, ""),
+ hub[CONF_TIMEOUT],
+ minimum_scan_interval - 1,
+ )
+ hub[CONF_TIMEOUT] = minimum_scan_interval - 1
+ return config
diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py
new file mode 100644
index 00000000000..a31b2655184
--- /dev/null
+++ b/homeassistant/components/modern_forms/__init__.py
@@ -0,0 +1,161 @@
+"""The Modern Forms integration."""
+from __future__ import annotations
+
+from datetime import timedelta
+import logging
+
+from aiomodernforms import (
+ ModernFormsConnectionError,
+ ModernFormsDevice,
+ ModernFormsError,
+)
+from aiomodernforms.models import Device as ModernFormsDeviceState
+
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
+from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, CONF_HOST
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
+
+from .const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, DOMAIN
+
+SCAN_INTERVAL = timedelta(seconds=5)
+PLATFORMS = [
+ BINARY_SENSOR_DOMAIN,
+ LIGHT_DOMAIN,
+ FAN_DOMAIN,
+ SENSOR_DOMAIN,
+ SWITCH_DOMAIN,
+]
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up a Modern Forms device from a config entry."""
+
+ # Create Modern Forms instance for this entry
+ coordinator = ModernFormsDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
+ await coordinator.async_config_entry_first_refresh()
+
+ hass.data.setdefault(DOMAIN, {})
+ hass.data[DOMAIN][entry.entry_id] = coordinator
+
+ # Set up all platforms for this device/entry.
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload Modern Forms config entry."""
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+
+ if unload_ok:
+ del hass.data[DOMAIN][entry.entry_id]
+
+ if not hass.data[DOMAIN]:
+ del hass.data[DOMAIN]
+
+ return unload_ok
+
+
+def modernforms_exception_handler(func):
+ """Decorate Modern Forms calls to handle Modern Forms exceptions.
+
+ A decorator that wraps the passed in function, catches Modern Forms errors,
+ and handles the availability of the device in the data coordinator.
+ """
+
+ async def handler(self, *args, **kwargs):
+ try:
+ await func(self, *args, **kwargs)
+ self.coordinator.update_listeners()
+
+ except ModernFormsConnectionError as error:
+ _LOGGER.error("Error communicating with API: %s", error)
+ self.coordinator.last_update_success = False
+ self.coordinator.update_listeners()
+
+ except ModernFormsError as error:
+ _LOGGER.error("Invalid response from API: %s", error)
+
+ return handler
+
+
+class ModernFormsDataUpdateCoordinator(DataUpdateCoordinator[ModernFormsDeviceState]):
+ """Class to manage fetching Modern Forms data from single endpoint."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ *,
+ host: str,
+ ) -> None:
+ """Initialize global Modern Forms data updater."""
+ self.modern_forms = ModernFormsDevice(
+ host, session=async_get_clientsession(hass)
+ )
+
+ super().__init__(
+ hass,
+ _LOGGER,
+ name=DOMAIN,
+ update_interval=SCAN_INTERVAL,
+ )
+
+ def update_listeners(self) -> None:
+ """Call update on all listeners."""
+ for update_callback in self._listeners:
+ update_callback()
+
+ async def _async_update_data(self) -> ModernFormsDevice:
+ """Fetch data from Modern Forms."""
+ try:
+ return await self.modern_forms.update(
+ full_update=not self.last_update_success
+ )
+ except ModernFormsError as error:
+ raise UpdateFailed(f"Invalid response from API: {error}") from error
+
+
+class ModernFormsDeviceEntity(CoordinatorEntity[ModernFormsDataUpdateCoordinator]):
+ """Defines a Modern Forms device entity."""
+
+ coordinator: ModernFormsDataUpdateCoordinator
+
+ def __init__(
+ self,
+ *,
+ entry_id: str,
+ coordinator: ModernFormsDataUpdateCoordinator,
+ name: str,
+ icon: str | None = None,
+ enabled_default: bool = True,
+ ) -> None:
+ """Initialize the Modern Forms entity."""
+ super().__init__(coordinator)
+ self._attr_enabled_default = enabled_default
+ self._entry_id = entry_id
+ self._attr_icon = icon
+ self._attr_name = name
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device information about this Modern Forms device."""
+ return {
+ ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)}, # type: ignore
+ ATTR_NAME: self.coordinator.data.info.device_name,
+ ATTR_MANUFACTURER: "Modern Forms",
+ ATTR_MODEL: self.coordinator.data.info.fan_type,
+ ATTR_SW_VERSION: f"{self.coordinator.data.info.firmware_version} / {self.coordinator.data.info.main_mcu_firmware_version}",
+ }
diff --git a/homeassistant/components/modern_forms/binary_sensor.py b/homeassistant/components/modern_forms/binary_sensor.py
new file mode 100644
index 00000000000..f8e3f8bbcf8
--- /dev/null
+++ b/homeassistant/components/modern_forms/binary_sensor.py
@@ -0,0 +1,114 @@
+"""Support for Modern Forms Binary Sensors."""
+from __future__ import annotations
+
+from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.util import dt as dt_util
+
+from . import ModernFormsDataUpdateCoordinator, ModernFormsDeviceEntity
+from .const import CLEAR_TIMER, DOMAIN
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up Modern Forms binary sensors."""
+ coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+
+ binary_sensors: list[ModernFormsBinarySensor] = [
+ ModernFormsFanSleepTimerActive(entry.entry_id, coordinator),
+ ]
+
+ # Only setup light sleep timer sensor if light unit installed
+ if coordinator.data.info.light_type:
+ binary_sensors.append(
+ ModernFormsLightSleepTimerActive(entry.entry_id, coordinator)
+ )
+
+ async_add_entities(binary_sensors)
+
+
+class ModernFormsBinarySensor(ModernFormsDeviceEntity, BinarySensorEntity):
+ """Defines a Modern Forms binary sensor."""
+
+ def __init__(
+ self,
+ *,
+ entry_id: str,
+ coordinator: ModernFormsDataUpdateCoordinator,
+ name: str,
+ icon: str,
+ key: str,
+ ) -> None:
+ """Initialize Modern Forms switch."""
+ super().__init__(
+ entry_id=entry_id, coordinator=coordinator, name=name, icon=icon
+ )
+
+ self._attr_unique_id = f"{coordinator.data.info.mac_address}_{key}"
+
+
+class ModernFormsLightSleepTimerActive(ModernFormsBinarySensor):
+ """Defines a Modern Forms Light Sleep Timer Active sensor."""
+
+ _attr_entity_registry_enabled_default = False
+
+ def __init__(
+ self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator
+ ) -> None:
+ """Initialize Modern Forms Light Sleep Timer Active sensor."""
+ super().__init__(
+ coordinator=coordinator,
+ entry_id=entry_id,
+ icon="mdi:av-timer",
+ key="light_sleep_timer_active",
+ name=f"{coordinator.data.info.device_name} Light Sleep Timer Active",
+ )
+
+ @property
+ def is_on(self) -> bool:
+ """Return the state of the timer."""
+ return not (
+ self.coordinator.data.state.light_sleep_timer == CLEAR_TIMER
+ or (
+ dt_util.utc_from_timestamp(
+ self.coordinator.data.state.light_sleep_timer
+ )
+ - dt_util.utcnow()
+ ).total_seconds()
+ < 0
+ )
+
+
+class ModernFormsFanSleepTimerActive(ModernFormsBinarySensor):
+ """Defines a Modern Forms Fan Sleep Timer Active sensor."""
+
+ _attr_entity_registry_enabled_default = False
+
+ def __init__(
+ self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator
+ ) -> None:
+ """Initialize Modern Forms Fan Sleep Timer Active sensor."""
+ super().__init__(
+ coordinator=coordinator,
+ entry_id=entry_id,
+ icon="mdi:av-timer",
+ key="fan_sleep_timer_active",
+ name=f"{coordinator.data.info.device_name} Fan Sleep Timer Active",
+ )
+
+ @property
+ def is_on(self) -> bool:
+ """Return the state of the timer."""
+ return not (
+ self.coordinator.data.state.fan_sleep_timer == CLEAR_TIMER
+ or (
+ dt_util.utc_from_timestamp(self.coordinator.data.state.fan_sleep_timer)
+ - dt_util.utcnow()
+ ).total_seconds()
+ < 0
+ )
diff --git a/homeassistant/components/modern_forms/config_flow.py b/homeassistant/components/modern_forms/config_flow.py
new file mode 100644
index 00000000000..e8b557f7bc5
--- /dev/null
+++ b/homeassistant/components/modern_forms/config_flow.py
@@ -0,0 +1,115 @@
+"""Config flow for Modern Forms."""
+from __future__ import annotations
+
+from typing import Any
+
+from aiomodernforms import ModernFormsConnectionError, ModernFormsDevice
+import voluptuous as vol
+
+from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow
+from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
+from homeassistant.data_entry_flow import FlowResult
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.typing import DiscoveryInfoType
+
+from .const import DOMAIN
+
+
+class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN):
+ """Handle a ModernForms config flow."""
+
+ VERSION = 1
+
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Handle setup by user for Modern Forms integration."""
+ return await self._handle_config_flow(user_input)
+
+ async def async_step_zeroconf(
+ self, discovery_info: DiscoveryInfoType
+ ) -> FlowResult:
+ """Handle zeroconf discovery."""
+ host = discovery_info["hostname"].rstrip(".")
+ name, _ = host.rsplit(".")
+
+ self.context.update(
+ {
+ CONF_HOST: discovery_info["host"],
+ CONF_NAME: name,
+ CONF_MAC: discovery_info["properties"].get(CONF_MAC),
+ "title_placeholders": {"name": name},
+ }
+ )
+
+ # Prepare configuration flow
+ return await self._handle_config_flow(discovery_info, True)
+
+ async def async_step_zeroconf_confirm(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Handle a flow initiated by zeroconf."""
+ return await self._handle_config_flow(user_input)
+
+ async def _handle_config_flow(
+ self, user_input: dict[str, Any] | None = None, prepare: bool = False
+ ) -> FlowResult:
+ """Config flow handler for ModernForms."""
+ source = self.context.get("source")
+
+ # Request user input, unless we are preparing discovery flow
+ if user_input is None:
+ user_input = {}
+ if not prepare:
+ if source == SOURCE_ZEROCONF:
+ return self._show_confirm_dialog()
+ return self._show_setup_form()
+
+ if source == SOURCE_ZEROCONF:
+ user_input[CONF_HOST] = self.context.get(CONF_HOST)
+ user_input[CONF_MAC] = self.context.get(CONF_MAC)
+
+ if user_input.get(CONF_MAC) is None or not prepare:
+ session = async_get_clientsession(self.hass)
+ device = ModernFormsDevice(user_input[CONF_HOST], session=session)
+ try:
+ device = await device.update()
+ except ModernFormsConnectionError:
+ if source == SOURCE_ZEROCONF:
+ return self.async_abort(reason="cannot_connect")
+ return self._show_setup_form({"base": "cannot_connect"})
+ user_input[CONF_MAC] = device.info.mac_address
+ user_input[CONF_NAME] = device.info.device_name
+
+ # Check if already configured
+ await self.async_set_unique_id(user_input[CONF_MAC])
+ self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]})
+
+ title = device.info.device_name
+ if source == SOURCE_ZEROCONF:
+ title = self.context.get(CONF_NAME)
+
+ if prepare:
+ return await self.async_step_zeroconf_confirm()
+
+ return self.async_create_entry(
+ title=title,
+ data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]},
+ )
+
+ def _show_setup_form(self, errors: dict | None = None) -> FlowResult:
+ """Show the setup form to the user."""
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
+ errors=errors or {},
+ )
+
+ def _show_confirm_dialog(self, errors: dict | None = None) -> FlowResult:
+ """Show the confirm dialog to the user."""
+ name = self.context.get(CONF_NAME)
+ return self.async_show_form(
+ step_id="zeroconf_confirm",
+ description_placeholders={"name": name},
+ errors=errors or {},
+ )
diff --git a/homeassistant/components/modern_forms/const.py b/homeassistant/components/modern_forms/const.py
new file mode 100644
index 00000000000..9dbefcfc570
--- /dev/null
+++ b/homeassistant/components/modern_forms/const.py
@@ -0,0 +1,19 @@
+"""Constants for the Modern Forms integration."""
+
+DOMAIN = "modern_forms"
+
+ATTR_IDENTIFIERS = "identifiers"
+ATTR_MANUFACTURER = "manufacturer"
+
+OPT_ON = "on"
+OPT_SPEED = "speed"
+OPT_BRIGHTNESS = "brightness"
+
+# Services
+SERVICE_SET_LIGHT_SLEEP_TIMER = "set_light_sleep_timer"
+SERVICE_CLEAR_LIGHT_SLEEP_TIMER = "clear_light_sleep_timer"
+SERVICE_SET_FAN_SLEEP_TIMER = "set_fan_sleep_timer"
+SERVICE_CLEAR_FAN_SLEEP_TIMER = "clear_fan_sleep_timer"
+
+ATTR_SLEEP_TIME = "sleep_time"
+CLEAR_TIMER = 0
diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py
new file mode 100644
index 00000000000..2668b26857b
--- /dev/null
+++ b/homeassistant/components/modern_forms/fan.py
@@ -0,0 +1,164 @@
+"""Support for Modern Forms Fan Fans."""
+from __future__ import annotations
+
+from typing import Any
+
+from aiomodernforms.const import FAN_POWER_OFF, FAN_POWER_ON
+import voluptuous as vol
+
+from homeassistant.components.fan import SUPPORT_DIRECTION, SUPPORT_SET_SPEED, FanEntity
+from homeassistant.config_entries import ConfigEntry
+import homeassistant.helpers.entity_platform as entity_platform
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.util.percentage import (
+ int_states_in_range,
+ percentage_to_ranged_value,
+ ranged_value_to_percentage,
+)
+
+from . import (
+ ModernFormsDataUpdateCoordinator,
+ ModernFormsDeviceEntity,
+ modernforms_exception_handler,
+)
+from .const import (
+ ATTR_SLEEP_TIME,
+ CLEAR_TIMER,
+ DOMAIN,
+ OPT_ON,
+ OPT_SPEED,
+ SERVICE_CLEAR_FAN_SLEEP_TIMER,
+ SERVICE_SET_FAN_SLEEP_TIMER,
+)
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up a Modern Forms platform from config entry."""
+
+ coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][
+ config_entry.entry_id
+ ]
+
+ platform = entity_platform.async_get_current_platform()
+
+ platform.async_register_entity_service(
+ SERVICE_SET_FAN_SLEEP_TIMER,
+ {
+ vol.Required(ATTR_SLEEP_TIME): vol.All(
+ vol.Coerce(int), vol.Range(min=1, max=1440)
+ ),
+ },
+ "async_set_fan_sleep_timer",
+ )
+
+ platform.async_register_entity_service(
+ SERVICE_CLEAR_FAN_SLEEP_TIMER,
+ {},
+ "async_clear_fan_sleep_timer",
+ )
+
+ async_add_entities(
+ [ModernFormsFanEntity(entry_id=config_entry.entry_id, coordinator=coordinator)]
+ )
+
+
+class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity):
+ """Defines a Modern Forms light."""
+
+ SPEED_RANGE = (1, 6) # off is not included
+
+ def __init__(
+ self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator
+ ) -> None:
+ """Initialize Modern Forms light."""
+ super().__init__(
+ entry_id=entry_id,
+ coordinator=coordinator,
+ name=f"{coordinator.data.info.device_name} Fan",
+ )
+ self._attr_unique_id = f"{self.coordinator.data.info.mac_address}"
+
+ @property
+ def supported_features(self) -> int:
+ """Flag supported features."""
+ return SUPPORT_DIRECTION | SUPPORT_SET_SPEED
+
+ @property
+ def percentage(self) -> int | None:
+ """Return the current speed percentage."""
+ percentage = 0
+ if bool(self.coordinator.data.state.fan_on):
+ percentage = ranged_value_to_percentage(
+ self.SPEED_RANGE, self.coordinator.data.state.fan_speed
+ )
+ return percentage
+
+ @property
+ def current_direction(self) -> str:
+ """Return the current direction of the fan."""
+ return self.coordinator.data.state.fan_direction
+
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return int_states_in_range(self.SPEED_RANGE)
+
+ @property
+ def is_on(self) -> bool:
+ """Return the state of the fan."""
+ return bool(self.coordinator.data.state.fan_on)
+
+ @modernforms_exception_handler
+ async def async_set_direction(self, direction: str) -> None:
+ """Set the direction of the fan."""
+ await self.coordinator.modern_forms.fan(direction=direction)
+
+ @modernforms_exception_handler
+ async def async_set_percentage(self, percentage: int) -> None:
+ """Set the speed percentage of the fan."""
+ if percentage > 0:
+ await self.async_turn_on(percentage=percentage)
+ else:
+ await self.async_turn_off()
+
+ @modernforms_exception_handler
+ async def async_turn_on(
+ self,
+ speed: int | None = None,
+ percentage: int | None = None,
+ preset_mode: int | None = None,
+ **kwargs: Any,
+ ) -> None:
+ """Turn on the fan."""
+ data = {OPT_ON: FAN_POWER_ON}
+
+ if percentage:
+ data[OPT_SPEED] = round(
+ percentage_to_ranged_value(self.SPEED_RANGE, percentage)
+ )
+ await self.coordinator.modern_forms.fan(**data)
+
+ @modernforms_exception_handler
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Turn the fan off."""
+ await self.coordinator.modern_forms.fan(on=FAN_POWER_OFF)
+
+ @modernforms_exception_handler
+ async def async_set_fan_sleep_timer(
+ self,
+ sleep_time: int,
+ ) -> None:
+ """Set a Modern Forms light sleep timer."""
+ await self.coordinator.modern_forms.fan(sleep=sleep_time * 60)
+
+ @modernforms_exception_handler
+ async def async_clear_fan_sleep_timer(
+ self,
+ ) -> None:
+ """Clear a Modern Forms fan sleep timer."""
+ await self.coordinator.modern_forms.fan(sleep=CLEAR_TIMER)
diff --git a/homeassistant/components/modern_forms/light.py b/homeassistant/components/modern_forms/light.py
new file mode 100644
index 00000000000..2c8298f00da
--- /dev/null
+++ b/homeassistant/components/modern_forms/light.py
@@ -0,0 +1,144 @@
+"""Support for Modern Forms Fan lights."""
+from __future__ import annotations
+
+from typing import Any
+
+from aiomodernforms.const import LIGHT_POWER_OFF, LIGHT_POWER_ON
+import voluptuous as vol
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ COLOR_MODE_BRIGHTNESS,
+ LightEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+import homeassistant.helpers.entity_platform as entity_platform
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.util.percentage import (
+ percentage_to_ranged_value,
+ ranged_value_to_percentage,
+)
+
+from . import (
+ ModernFormsDataUpdateCoordinator,
+ ModernFormsDeviceEntity,
+ modernforms_exception_handler,
+)
+from .const import (
+ ATTR_SLEEP_TIME,
+ CLEAR_TIMER,
+ DOMAIN,
+ OPT_BRIGHTNESS,
+ OPT_ON,
+ SERVICE_CLEAR_LIGHT_SLEEP_TIMER,
+ SERVICE_SET_LIGHT_SLEEP_TIMER,
+)
+
+BRIGHTNESS_RANGE = (1, 255)
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up a Modern Forms platform from config entry."""
+
+ coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][
+ config_entry.entry_id
+ ]
+
+ # if no light unit installed no light entity
+ if not coordinator.data.info.light_type:
+ return
+
+ platform = entity_platform.async_get_current_platform()
+
+ platform.async_register_entity_service(
+ SERVICE_SET_LIGHT_SLEEP_TIMER,
+ {
+ vol.Required(ATTR_SLEEP_TIME): vol.All(
+ vol.Coerce(int), vol.Range(min=1, max=1440)
+ ),
+ },
+ "async_set_light_sleep_timer",
+ )
+
+ platform.async_register_entity_service(
+ SERVICE_CLEAR_LIGHT_SLEEP_TIMER,
+ {},
+ "async_clear_light_sleep_timer",
+ )
+
+ async_add_entities(
+ [
+ ModernFormsLightEntity(
+ entry_id=config_entry.entry_id, coordinator=coordinator
+ )
+ ]
+ )
+
+
+class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity):
+ """Defines a Modern Forms light."""
+
+ def __init__(
+ self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator
+ ) -> None:
+ """Initialize Modern Forms light."""
+ super().__init__(
+ entry_id=entry_id,
+ coordinator=coordinator,
+ name=f"{coordinator.data.info.device_name} Light",
+ icon=None,
+ )
+ self._attr_unique_id = f"{self.coordinator.data.info.mac_address}"
+ self._attr_color_mode = COLOR_MODE_BRIGHTNESS
+ self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS}
+
+ @property
+ def brightness(self) -> int | None:
+ """Return the brightness of this light between 1..255."""
+ return round(
+ percentage_to_ranged_value(
+ BRIGHTNESS_RANGE, self.coordinator.data.state.light_brightness
+ )
+ )
+
+ @property
+ def is_on(self) -> bool:
+ """Return the state of the light."""
+ return bool(self.coordinator.data.state.light_on)
+
+ @modernforms_exception_handler
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Turn off the light."""
+ await self.coordinator.modern_forms.light(on=LIGHT_POWER_OFF)
+
+ @modernforms_exception_handler
+ async def async_turn_on(self, **kwargs: Any) -> None:
+ """Turn on the light."""
+ data = {OPT_ON: LIGHT_POWER_ON}
+
+ if ATTR_BRIGHTNESS in kwargs:
+ data[OPT_BRIGHTNESS] = ranged_value_to_percentage(
+ BRIGHTNESS_RANGE, kwargs[ATTR_BRIGHTNESS]
+ )
+
+ await self.coordinator.modern_forms.light(**data)
+
+ @modernforms_exception_handler
+ async def async_set_light_sleep_timer(
+ self,
+ sleep_time: int,
+ ) -> None:
+ """Set a Modern Forms light sleep timer."""
+ await self.coordinator.modern_forms.light(sleep=sleep_time * 60)
+
+ @modernforms_exception_handler
+ async def async_clear_light_sleep_timer(
+ self,
+ ) -> None:
+ """Clear a Modern Forms light sleep timer."""
+ await self.coordinator.modern_forms.light(sleep=CLEAR_TIMER)
diff --git a/homeassistant/components/modern_forms/manifest.json b/homeassistant/components/modern_forms/manifest.json
new file mode 100644
index 00000000000..1466537259b
--- /dev/null
+++ b/homeassistant/components/modern_forms/manifest.json
@@ -0,0 +1,16 @@
+{
+ "domain": "modern_forms",
+ "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"
+ ],
+ "iot_class": "local_polling"
+}
diff --git a/homeassistant/components/modern_forms/sensor.py b/homeassistant/components/modern_forms/sensor.py
new file mode 100644
index 00000000000..01efe3f1d28
--- /dev/null
+++ b/homeassistant/components/modern_forms/sensor.py
@@ -0,0 +1,118 @@
+"""Support for Modern Forms switches."""
+from __future__ import annotations
+
+from datetime import datetime
+
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import DEVICE_CLASS_TIMESTAMP
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import StateType
+from homeassistant.util import dt as dt_util
+
+from . import ModernFormsDataUpdateCoordinator, ModernFormsDeviceEntity
+from .const import CLEAR_TIMER, DOMAIN
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up Modern Forms sensor based on a config entry."""
+ coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+
+ sensors: list[ModernFormsSensor] = [
+ ModernFormsFanTimerRemainingTimeSensor(entry.entry_id, coordinator),
+ ]
+
+ # Only setup light sleep timer sensor if light unit installed
+ if coordinator.data.info.light_type:
+ sensors.append(
+ ModernFormsLightTimerRemainingTimeSensor(entry.entry_id, coordinator)
+ )
+
+ async_add_entities(sensors)
+
+
+class ModernFormsSensor(ModernFormsDeviceEntity, SensorEntity):
+ """Defines a Modern Forms binary sensor."""
+
+ def __init__(
+ self,
+ *,
+ entry_id: str,
+ coordinator: ModernFormsDataUpdateCoordinator,
+ name: str,
+ icon: str,
+ key: str,
+ ) -> None:
+ """Initialize Modern Forms switch."""
+ self._key = key
+ super().__init__(
+ entry_id=entry_id, coordinator=coordinator, name=name, icon=icon
+ )
+ self._attr_unique_id = f"{self.coordinator.data.info.mac_address}_{self._key}"
+
+
+class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor):
+ """Defines the Modern Forms Light Timer remaining time sensor."""
+
+ def __init__(
+ self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator
+ ) -> None:
+ """Initialize Modern Forms Away mode switch."""
+ super().__init__(
+ coordinator=coordinator,
+ entry_id=entry_id,
+ icon="mdi:timer-outline",
+ key="light_timer_remaining_time",
+ name=f"{coordinator.data.info.device_name} Light Sleep Time",
+ )
+ self._attr_device_class = DEVICE_CLASS_TIMESTAMP
+
+ @property
+ def state(self) -> StateType:
+ """Return the state of the sensor."""
+ sleep_time: datetime = dt_util.utc_from_timestamp(
+ self.coordinator.data.state.light_sleep_timer
+ )
+ if (
+ self.coordinator.data.state.light_sleep_timer == CLEAR_TIMER
+ or (sleep_time - dt_util.utcnow()).total_seconds() < 0
+ ):
+ return None
+ return sleep_time.isoformat()
+
+
+class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor):
+ """Defines the Modern Forms Light Timer remaining time sensor."""
+
+ def __init__(
+ self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator
+ ) -> None:
+ """Initialize Modern Forms Away mode switch."""
+ super().__init__(
+ coordinator=coordinator,
+ entry_id=entry_id,
+ icon="mdi:timer-outline",
+ key="fan_timer_remaining_time",
+ name=f"{coordinator.data.info.device_name} Fan Sleep Time",
+ )
+ self._attr_device_class = DEVICE_CLASS_TIMESTAMP
+
+ @property
+ def state(self) -> StateType:
+ """Return the state of the sensor."""
+ sleep_time: datetime = dt_util.utc_from_timestamp(
+ self.coordinator.data.state.fan_sleep_timer
+ )
+
+ if (
+ self.coordinator.data.state.fan_sleep_timer == CLEAR_TIMER
+ or (sleep_time - dt_util.utcnow()).total_seconds() < 0
+ ):
+ return None
+
+ return sleep_time.isoformat()
diff --git a/homeassistant/components/modern_forms/services.yaml b/homeassistant/components/modern_forms/services.yaml
new file mode 100644
index 00000000000..ce3c29f39b5
--- /dev/null
+++ b/homeassistant/components/modern_forms/services.yaml
@@ -0,0 +1,50 @@
+set_light_sleep_timer:
+ name: Set light sleep timer
+ description: Set a sleep timer on a Modern Forms light.
+ target:
+ entity:
+ integration: modern_forms
+ domain: light
+ fields:
+ sleep_time:
+ name: Sleep Time
+ description: Number of minutes to set the timer.
+ required: true
+ example: "900"
+ selector:
+ number:
+ min: 1
+ max: 1440
+ unit_of_measurement: minutes
+clear_light_sleep_timer:
+ name: Clear light sleep timer
+ description: Clear the sleep timer on a Modern Forms light.
+ target:
+ entity:
+ integration: modern_forms
+ domain: light
+set_fan_sleep_timer:
+ name: Set fan sleep timer
+ description: Set a sleep timer on a Modern Forms fan.
+ target:
+ entity:
+ integration: modern_forms
+ domain: fan
+ fields:
+ sleep_time:
+ name: Sleep Time
+ description: Number of minutes to set the timer.
+ required: true
+ example: "900"
+ selector:
+ number:
+ min: 1
+ max: 1440
+ unit_of_measurement: minutes
+clear_fan_sleep_timer:
+ name: Clear fan sleep timer
+ description: Clear the sleep timer on a Modern Forms fan.
+ target:
+ entity:
+ integration: modern_forms
+ domain: fan
diff --git a/homeassistant/components/modern_forms/strings.json b/homeassistant/components/modern_forms/strings.json
new file mode 100644
index 00000000000..fc30709960b
--- /dev/null
+++ b/homeassistant/components/modern_forms/strings.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "flow_title": "{name}",
+ "step": {
+ "user": {
+ "description": "Set up your Modern Forms fan to integrate with Home Assistant.",
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]"
+ }
+ },
+ "zeroconf_confirm": {
+ "description": "Do you want to add the Modern Forms fan named `{name}` to Home Assistant?",
+ "title": "Discovered Modern Forms fan device"
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ }
+ }
+}
diff --git a/homeassistant/components/modern_forms/switch.py b/homeassistant/components/modern_forms/switch.py
new file mode 100644
index 00000000000..90d5d13d649
--- /dev/null
+++ b/homeassistant/components/modern_forms/switch.py
@@ -0,0 +1,113 @@
+"""Support for Modern Forms switches."""
+from __future__ import annotations
+
+from typing import Any
+
+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 . import (
+ ModernFormsDataUpdateCoordinator,
+ ModernFormsDeviceEntity,
+ modernforms_exception_handler,
+)
+from .const import DOMAIN
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up Modern Forms switch based on a config entry."""
+ coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+
+ switches = [
+ ModernFormsAwaySwitch(entry.entry_id, coordinator),
+ ModernFormsAdaptiveLearningSwitch(entry.entry_id, coordinator),
+ ]
+ async_add_entities(switches)
+
+
+class ModernFormsSwitch(ModernFormsDeviceEntity, SwitchEntity):
+ """Defines a Modern Forms switch."""
+
+ def __init__(
+ self,
+ *,
+ entry_id: str,
+ coordinator: ModernFormsDataUpdateCoordinator,
+ name: str,
+ icon: str,
+ key: str,
+ ) -> None:
+ """Initialize Modern Forms switch."""
+ self._key = key
+ super().__init__(
+ entry_id=entry_id, coordinator=coordinator, name=name, icon=icon
+ )
+ self._attr_unique_id = f"{self.coordinator.data.info.mac_address}_{self._key}"
+
+
+class ModernFormsAwaySwitch(ModernFormsSwitch):
+ """Defines a Modern Forms Away mode switch."""
+
+ def __init__(
+ self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator
+ ) -> None:
+ """Initialize Modern Forms Away mode switch."""
+ super().__init__(
+ coordinator=coordinator,
+ entry_id=entry_id,
+ icon="mdi:airplane-takeoff",
+ key="away_mode",
+ name=f"{coordinator.data.info.device_name} Away Mode",
+ )
+
+ @property
+ def is_on(self) -> bool:
+ """Return the state of the switch."""
+ return bool(self.coordinator.data.state.away_mode_enabled)
+
+ @modernforms_exception_handler
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Turn off the Modern Forms Away mode switch."""
+ await self.coordinator.modern_forms.away(away=False)
+
+ @modernforms_exception_handler
+ async def async_turn_on(self, **kwargs: Any) -> None:
+ """Turn on the Modern Forms Away mode switch."""
+ await self.coordinator.modern_forms.away(away=True)
+
+
+class ModernFormsAdaptiveLearningSwitch(ModernFormsSwitch):
+ """Defines a Modern Forms Adaptive Learning switch."""
+
+ def __init__(
+ self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator
+ ) -> None:
+ """Initialize Modern Forms Adaptive Learning switch."""
+ super().__init__(
+ coordinator=coordinator,
+ entry_id=entry_id,
+ icon="mdi:school-outline",
+ key="adaptive_learning",
+ name=f"{coordinator.data.info.device_name} Adaptive Learning",
+ )
+
+ @property
+ def is_on(self) -> bool:
+ """Return the state of the switch."""
+ return bool(self.coordinator.data.state.adaptive_learning_enabled)
+
+ @modernforms_exception_handler
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Turn off the Modern Forms Adaptive Learning switch."""
+ await self.coordinator.modern_forms.adaptive_learning(adaptive_learning=False)
+
+ @modernforms_exception_handler
+ async def async_turn_on(self, **kwargs: Any) -> None:
+ """Turn on the Modern Forms Adaptive Learning switch."""
+ await self.coordinator.modern_forms.adaptive_learning(adaptive_learning=True)
diff --git a/homeassistant/components/modern_forms/translations/ca.json b/homeassistant/components/modern_forms/translations/ca.json
new file mode 100644
index 00000000000..cea3bc7b685
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/ca.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "description": "Vols comen\u00e7ar la configuraci\u00f3?"
+ },
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3"
+ },
+ "description": "Configura el teu ventilador Modern Forms per integrar-lo a Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "description": "Vols afegir el ventilador de Modern Forms anomenat `{name}` a Home Assistant?",
+ "title": "Dispositiu ventilador Modern Forms descobert"
+ }
+ }
+ },
+ "title": "Modern Forms"
+}
\ No newline at end of file
diff --git a/homeassistant/components/modern_forms/translations/de.json b/homeassistant/components/modern_forms/translations/de.json
new file mode 100644
index 00000000000..644f525179e
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/de.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "description": "M\u00f6chten Sie mit der Einrichtung beginnen?"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Einrichten Ihres Modern Forms Ventilator f\u00fcr die Integration in Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "description": "M\u00f6chten Sie den Modern Forms Ventilator mit dem Namen `{name}` zu Home Assistant hinzuf\u00fcgen?",
+ "title": "Erkannter Modern Forms Ventilator"
+ }
+ }
+ },
+ "title": "Modern Forms"
+}
\ No newline at end of file
diff --git a/homeassistant/components/modern_forms/translations/en.json b/homeassistant/components/modern_forms/translations/en.json
new file mode 100644
index 00000000000..6c51e42a2d1
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/en.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured",
+ "cannot_connect": "Failed to connect"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "description": "Do you want to start set up?"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Set up your Modern Forms fan to integrate with Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "description": "Do you want to add the Modern Forms fan named `{name}` to Home Assistant?",
+ "title": "Discovered Modern Forms fan device"
+ }
+ }
+ },
+ "title": "Modern Forms"
+}
\ No newline at end of file
diff --git a/homeassistant/components/modern_forms/translations/es.json b/homeassistant/components/modern_forms/translations/es.json
new file mode 100644
index 00000000000..ac911baf4a4
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/es.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "flow_title": "{name}",
+ "step": {
+ "user": {
+ "description": "Configura tu ventilador de Modern Forms para que se integre con Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "description": "\u00bfQuieres a\u00f1adir el ventilador de Modern Forms llamado `{name}` a Home Assistant?",
+ "title": "Dispositivo de ventilador de Modern Forms descubierto"
+ }
+ }
+ },
+ "title": "Modern Forms"
+}
\ No newline at end of file
diff --git a/homeassistant/components/modern_forms/translations/et.json b/homeassistant/components/modern_forms/translations/et.json
new file mode 100644
index 00000000000..ee325440c13
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/et.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "cannot_connect": "\u00dchendamine nurjus"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "description": "Kas soovid alustada seadistamist?"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Seadista Modern Forms'i ventilaator sidumiseks Home Assistantiga."
+ },
+ "zeroconf_confirm": {
+ "description": "Kas lisada Modern Forms'i ventilaator nimega `{nimi}` Home Assistanti?",
+ "title": "Leitud Modern Forms ventilaator"
+ }
+ }
+ },
+ "title": "Modern Forms"
+}
\ No newline at end of file
diff --git a/homeassistant/components/modern_forms/translations/he.json b/homeassistant/components/modern_forms/translations/he.json
new file mode 100644
index 00000000000..701a3689598
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/he.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "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/modern_forms/translations/hu.json b/homeassistant/components/modern_forms/translations/hu.json
new file mode 100644
index 00000000000..b64bf60763a
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/hu.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?"
+ },
+ "user": {
+ "data": {
+ "host": "Hoszt"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/modern_forms/translations/it.json b/homeassistant/components/modern_forms/translations/it.json
new file mode 100644
index 00000000000..18f1d5f503a
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/it.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "description": "Vuoi iniziare la configurazione?"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Configura il tuo ventilatore Modern Forms per integrarlo con Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "description": "Vuoi aggiungere il ventilatore di Modern Forms chiamato `{name}` a Home Assistant?",
+ "title": "Rilevato il dispositivo ventilatore di Modern Forms"
+ }
+ }
+ },
+ "title": "Modern Forms"
+}
\ No newline at end of file
diff --git a/homeassistant/components/modern_forms/translations/nl.json b/homeassistant/components/modern_forms/translations/nl.json
new file mode 100644
index 00000000000..5a3d63e15a7
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/nl.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "description": "Wil je beginnen met instellen?"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Stel uw Modern Forms ventilator in om te integreren met Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "description": "Wil je de Modern Forms-fan met de naam ` {name} ` toevoegen aan Home Assistant?",
+ "title": "Ontdekt Modern Forms ventilator apparaat"
+ }
+ }
+ },
+ "title": "Modern Forms"
+}
\ No newline at end of file
diff --git a/homeassistant/components/modern_forms/translations/no.json b/homeassistant/components/modern_forms/translations/no.json
new file mode 100644
index 00000000000..04718b9d039
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/no.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "description": "Vil du starte oppsettet?"
+ },
+ "user": {
+ "data": {
+ "host": "Vert"
+ },
+ "description": "Sett opp Modern Forms-fanen din for \u00e5 integrere med Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "description": "Vil du legge til Modern Forms-viften med navnet {name} i Hjemmeassistent?",
+ "title": "Oppdaget Modern Forms-vifteenhet"
+ }
+ }
+ },
+ "title": "Moderne former"
+}
\ No newline at end of file
diff --git a/homeassistant/components/modern_forms/translations/pl.json b/homeassistant/components/modern_forms/translations/pl.json
new file mode 100644
index 00000000000..f68be10b7cd
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/pl.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?"
+ },
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP"
+ },
+ "description": "Skonfiguruj Modern Forms, aby zintegrowa\u0107 go z Home Assistantem."
+ },
+ "zeroconf_confirm": {
+ "description": "Czy chcesz doda\u0107 wentylator Modern Forms o nazwie {name} do Home Assistanta?",
+ "title": "Wykryto wentylator Modern Forms"
+ }
+ }
+ },
+ "title": "Modern Forms"
+}
\ No newline at end of file
diff --git a/homeassistant/components/modern_forms/translations/ru.json b/homeassistant/components/modern_forms/translations/ru.json
new file mode 100644
index 00000000000..9bfba30bf33
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/ru.json
@@ -0,0 +1,28 @@
+{
+ "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.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440\u043e\u043c Modern Forms."
+ },
+ "zeroconf_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 \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440 Modern Forms `{name}`?",
+ "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Modern Forms"
+ }
+ }
+ },
+ "title": "Modern Forms"
+}
\ No newline at end of file
diff --git a/homeassistant/components/modern_forms/translations/zh-Hant.json b/homeassistant/components/modern_forms/translations/zh-Hant.json
new file mode 100644
index 00000000000..df3ebf486c7
--- /dev/null
+++ b/homeassistant/components/modern_forms/translations/zh-Hant.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f"
+ },
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef"
+ },
+ "description": "\u8a2d\u5b9a Modern Forms \u98a8\u6247\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002"
+ },
+ "zeroconf_confirm": {
+ "description": "\u662f\u5426\u8981\u65b0\u589e\u540d\u7a31 `{name}` Modern Forms \u98a8\u6247\u81f3 Home Assistant\uff1f",
+ "title": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Modern Forms \u98a8\u6247\u88dd\u7f6e"
+ }
+ }
+ },
+ "title": "Modern Forms"
+}
\ No newline at end of file
diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py
index f543220b5b9..9ee6128c784 100644
--- a/homeassistant/components/monoprice/__init__.py
+++ b/homeassistant/components/monoprice/__init__.py
@@ -22,7 +22,7 @@ PLATFORMS = ["media_player"]
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Monoprice 6-Zone Amplifier from a config entry."""
port = entry.data[CONF_PORT]
diff --git a/homeassistant/components/monoprice/translations/he.json b/homeassistant/components/monoprice/translations/he.json
new file mode 100644
index 00000000000..80169dd26e7
--- /dev/null
+++ b/homeassistant/components/monoprice/translations/he.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/moon/translations/sensor.he.json b/homeassistant/components/moon/translations/sensor.he.json
new file mode 100644
index 00000000000..918f8fec5f9
--- /dev/null
+++ b/homeassistant/components/moon/translations/sensor.he.json
@@ -0,0 +1,14 @@
+{
+ "state": {
+ "moon__phase": {
+ "first_quarter": "\u05e8\u05d1\u05e2\u05d5\u05df \u05e8\u05d0\u05e9\u05d5\u05df",
+ "full_moon": "\u05d9\u05e8\u05d7 \u05de\u05dc\u05d0",
+ "last_quarter": "\u05e8\u05d1\u05e2\u05d5\u05df \u05d0\u05d7\u05e8\u05d5\u05df",
+ "new_moon": "\u05d9\u05e8\u05d7 \u05d7\u05d3\u05e9",
+ "waning_crescent": "\u05e1\u05d4\u05e8 \u05d3\u05d5\u05e2\u05da",
+ "waning_gibbous": "\u05d2\u05d1\u05e2\u05d5\u05dc\u05d9 \u05e0\u05d5\u05d3\u05d3",
+ "waxing_crescent": "\u05d7\u05e6\u05d9 \u05e1\u05d4\u05e8 \u05e9\u05e2\u05d5\u05d5\u05d4",
+ "waxing_gibbous": "\u05e1\u05d9\u05d1\u05d9\u05ea \u05e9\u05e2\u05d5\u05d5\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py
index d2400beb4f5..f7ae6573b1b 100644
--- a/homeassistant/components/motion_blinds/__init__.py
+++ b/homeassistant/components/motion_blinds/__init__.py
@@ -80,12 +80,7 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator):
"""Fetch the latest data from the gateway and blinds."""
data = await self.hass.async_add_executor_job(self.update_gateway)
- all_available = True
- for device in data.values():
- if not device[ATTR_AVAILABLE]:
- all_available = False
- break
-
+ all_available = all(device[ATTR_AVAILABLE] for device in data.values())
if all_available:
self.update_interval = timedelta(seconds=UPDATE_INTERVAL)
else:
@@ -94,11 +89,6 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator):
return data
-def setup(hass: core.HomeAssistant, config: dict):
- """Set up the Motion Blinds component."""
- return True
-
-
async def async_setup_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py
index a802ecfb667..60ec375a9c0 100644
--- a/homeassistant/components/motion_blinds/cover.py
+++ b/homeassistant/components/motion_blinds/cover.py
@@ -132,32 +132,19 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity):
super().__init__(coordinator)
self._blind = blind
- self._device_class = device_class
self._config_entry = config_entry
- @property
- def unique_id(self):
- """Return the unique id of the blind."""
- return self._blind.mac
-
- @property
- def device_info(self):
- """Return the device info of the blind."""
- device_info = {
- "identifiers": {(DOMAIN, self._blind.mac)},
+ self._attr_device_class = device_class
+ self._attr_name = f"{blind.blind_type}-{blind.mac[12:]}"
+ self._attr_unique_id = blind.mac
+ self._attr_device_info = {
+ "identifiers": {(DOMAIN, blind.mac)},
"manufacturer": MANUFACTURER,
- "name": f"{self._blind.blind_type}-{self._blind.mac[12:]}",
- "model": self._blind.blind_type,
- "via_device": (DOMAIN, self._config_entry.unique_id),
+ "name": f"{blind.blind_type}-{blind.mac[12:]}",
+ "model": blind.blind_type,
+ "via_device": (DOMAIN, config_entry.unique_id),
}
- return device_info
-
- @property
- def name(self):
- """Return the name of the blind."""
- return f"{self._blind.blind_type}-{self._blind.mac[12:]}"
-
@property
def available(self):
"""Return True if entity is available."""
@@ -180,11 +167,6 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity):
return None
return 100 - self._blind.position
- @property
- def device_class(self):
- """Return the device class."""
- return self._device_class
-
@property
def is_closed(self):
"""Return if the cover is closed or not."""
@@ -263,20 +245,12 @@ class MotionTDBUDevice(MotionPositionDevice):
super().__init__(coordinator, blind, device_class, config_entry)
self._motor = motor
self._motor_key = motor[0]
+ self._attr_name = f"{blind.blind_type}-{motor}-{blind.mac[12:]}"
+ self._attr_unique_id = f"{blind.mac}-{motor}"
if self._motor not in ["Bottom", "Top", "Combined"]:
_LOGGER.error("Unknown motor '%s'", self._motor)
- @property
- def unique_id(self):
- """Return the unique id of the blind."""
- return f"{self._blind.mac}-{self._motor}"
-
- @property
- def name(self):
- """Return the name of the blind."""
- return f"{self._blind.blind_type}-{self._motor}-{self._blind.mac[12:]}"
-
@property
def current_cover_position(self):
"""
diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py
index 0da38795f7b..be88a099f25 100644
--- a/homeassistant/components/motion_blinds/sensor.py
+++ b/homeassistant/components/motion_blinds/sensor.py
@@ -46,26 +46,17 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity):
Updates are done by the cover platform.
"""
+ _attr_device_class = DEVICE_CLASS_BATTERY
+ _attr_unit_of_measurement = PERCENTAGE
+
def __init__(self, coordinator, blind):
"""Initialize the Motion Battery Sensor."""
super().__init__(coordinator)
self._blind = blind
-
- @property
- def unique_id(self):
- """Return the unique id of the blind."""
- return f"{self._blind.mac}-battery"
-
- @property
- def device_info(self):
- """Return the device info of the blind."""
- return {"identifiers": {(DOMAIN, self._blind.mac)}}
-
- @property
- def name(self):
- """Return the name of the blind battery sensor."""
- return f"{self._blind.blind_type}-battery-{self._blind.mac[12:]}"
+ self._attr_device_info = {"identifiers": {(DOMAIN, blind.mac)}}
+ self._attr_name = f"{blind.blind_type}-battery-{blind.mac[12:]}"
+ self._attr_unique_id = f"{blind.mac}-battery"
@property
def available(self):
@@ -78,16 +69,6 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity):
return self.coordinator.data[self._blind.mac][ATTR_AVAILABLE]
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return PERCENTAGE
-
- @property
- def device_class(self):
- """Return the device class of this entity."""
- return DEVICE_CLASS_BATTERY
-
@property
def state(self):
"""Return the state of the sensor."""
@@ -121,16 +102,8 @@ class MotionTDBUBatterySensor(MotionBatterySensor):
super().__init__(coordinator, blind)
self._motor = motor
-
- @property
- def unique_id(self):
- """Return the unique id of the blind."""
- return f"{self._blind.mac}-{self._motor}-battery"
-
- @property
- def name(self):
- """Return the name of the blind battery sensor."""
- return f"{self._blind.blind_type}-{self._motor}-battery-{self._blind.mac[12:]}"
+ self._attr_unique_id = f"{blind.mac}-{motor}-battery"
+ self._attr_name = f"{blind.blind_type}-{motor}-battery-{blind.mac[12:]}"
@property
def state(self):
@@ -153,22 +126,18 @@ class MotionTDBUBatterySensor(MotionBatterySensor):
class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity):
"""Representation of a Motion Signal Strength Sensor."""
+ _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH
+ _attr_entity_registry_enabled_default = False
+ _attr_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+
def __init__(self, coordinator, device, device_type):
"""Initialize the Motion Signal Strength Sensor."""
super().__init__(coordinator)
self._device = device
self._device_type = device_type
-
- @property
- def unique_id(self):
- """Return the unique id of the blind."""
- return f"{self._device.mac}-RSSI"
-
- @property
- def device_info(self):
- """Return the device info of the blind."""
- return {"identifiers": {(DOMAIN, self._device.mac)}}
+ self._attr_device_info = {"identifiers": {(DOMAIN, device.mac)}}
+ self._attr_unique_id = f"{device.mac}-RSSI"
@property
def name(self):
@@ -192,21 +161,6 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity):
and self.coordinator.data[self._device.mac][ATTR_AVAILABLE]
)
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return SIGNAL_STRENGTH_DECIBELS_MILLIWATT
-
- @property
- def device_class(self):
- """Return the device class of this entity."""
- return DEVICE_CLASS_SIGNAL_STRENGTH
-
- @property
- def entity_registry_enabled_default(self):
- """Return if the entity should be enabled when first added to the entity registry."""
- return False
-
@property
def state(self):
"""Return the state of the sensor."""
diff --git a/homeassistant/components/motion_blinds/translations/he.json b/homeassistant/components/motion_blinds/translations/he.json
new file mode 100644
index 00000000000..0876de6504a
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/he.json
@@ -0,0 +1,27 @@
+{
+ "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",
+ "connection_error": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "connect": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API"
+ }
+ },
+ "select": {
+ "data": {
+ "select_ip": "\u05db\u05ea\u05d5\u05d1\u05ea IP"
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motioneye/translations/ca.json b/homeassistant/components/motioneye/translations/ca.json
index 65ce7e48781..8f11dba2802 100644
--- a/homeassistant/components/motioneye/translations/ca.json
+++ b/homeassistant/components/motioneye/translations/ca.json
@@ -11,6 +11,10 @@
"unknown": "Error inesperat"
},
"step": {
+ "hassio_confirm": {
+ "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el servei motionEye proporcionat pel complement: {addon}?",
+ "title": "motionEye via complement de Home Assistant"
+ },
"user": {
"data": {
"admin_password": "Contrasenya d'administrador",
diff --git a/homeassistant/components/motioneye/translations/de.json b/homeassistant/components/motioneye/translations/de.json
index 94b86f04b2d..3370717366d 100644
--- a/homeassistant/components/motioneye/translations/de.json
+++ b/homeassistant/components/motioneye/translations/de.json
@@ -11,6 +11,10 @@
"unknown": "Unerwarteter Fehler"
},
"step": {
+ "hassio_confirm": {
+ "description": "M\u00f6chten Sie Home Assistant so konfigurieren, dass er sich mit dem motionEye-Dienst des Add-ons {addon} verbindet?",
+ "title": "motionEye \u00fcber Home Assistant Add-on"
+ },
"user": {
"data": {
"admin_password": "Admin Passwort",
diff --git a/homeassistant/components/motioneye/translations/es.json b/homeassistant/components/motioneye/translations/es.json
index 4f749d5c6d8..d018c52515e 100644
--- a/homeassistant/components/motioneye/translations/es.json
+++ b/homeassistant/components/motioneye/translations/es.json
@@ -11,6 +11,10 @@
"unknown": "Error inesperado"
},
"step": {
+ "hassio_confirm": {
+ "description": "\u00bfQuieres configurar Home Assistant para que se conecte al servicio motionEye proporcionado por el complemento: {addon}?",
+ "title": "motionEye a trav\u00e9s del complemento Home Assistant"
+ },
"user": {
"data": {
"admin_password": "Contrase\u00f1a administrador",
diff --git a/homeassistant/components/motioneye/translations/et.json b/homeassistant/components/motioneye/translations/et.json
index c3e44c52974..1b0861dbc75 100644
--- a/homeassistant/components/motioneye/translations/et.json
+++ b/homeassistant/components/motioneye/translations/et.json
@@ -11,6 +11,10 @@
"unknown": "Tundmatu viga"
},
"step": {
+ "hassio_confirm": {
+ "description": "Kas konfigureerida Home Assistanti \u00fchenduse loomiseks lisandmooduli pakutava motionEye teenusega: {addon} ?",
+ "title": "motionEye Home Assistanti lisandmooduli kaudu"
+ },
"user": {
"data": {
"admin_password": "Haldaja salas\u00f5na",
diff --git a/homeassistant/components/motioneye/translations/he.json b/homeassistant/components/motioneye/translations/he.json
new file mode 100644
index 00000000000..0397abd99b8
--- /dev/null
+++ b/homeassistant/components/motioneye/translations/he.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
+ "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",
+ "invalid_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "admin_password": "\u05e1\u05d9\u05e1\u05de\u05ea \u05de\u05e0\u05d4\u05dc \u05d4\u05de\u05e2\u05e8\u05db\u05ea",
+ "admin_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05de\u05e0\u05d4\u05dc \u05de\u05e2\u05e8\u05db\u05ea",
+ "surveillance_password": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05de\u05e9\u05d2\u05d9\u05d7",
+ "surveillance_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05de\u05e9\u05d2\u05d9\u05d7",
+ "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motioneye/translations/it.json b/homeassistant/components/motioneye/translations/it.json
index af07fac1a94..27e1167b3db 100644
--- a/homeassistant/components/motioneye/translations/it.json
+++ b/homeassistant/components/motioneye/translations/it.json
@@ -11,6 +11,10 @@
"unknown": "Errore imprevisto"
},
"step": {
+ "hassio_confirm": {
+ "description": "Vuoi configurare Home Assistant per connettersi al servizio motionEye fornito dal componente aggiuntivo: {addon}?",
+ "title": "motionEye tramite il componente aggiuntivo Home Assistant"
+ },
"user": {
"data": {
"admin_password": "Amministratore Password",
diff --git a/homeassistant/components/motioneye/translations/nl.json b/homeassistant/components/motioneye/translations/nl.json
index 07d8dc71a10..0fd3c7661eb 100644
--- a/homeassistant/components/motioneye/translations/nl.json
+++ b/homeassistant/components/motioneye/translations/nl.json
@@ -11,6 +11,10 @@
"unknown": "Onverwachte fout"
},
"step": {
+ "hassio_confirm": {
+ "description": "Wilt u Home Assistant configureren om verbinding te maken met de motionEye-service die wordt geleverd door de add-on: {addon}?",
+ "title": "motionEye via Home Assistant add-on"
+ },
"user": {
"data": {
"admin_password": "Admin Wachtwoord",
diff --git a/homeassistant/components/motioneye/translations/no.json b/homeassistant/components/motioneye/translations/no.json
index 5b7f6538bb8..33dadffa94f 100644
--- a/homeassistant/components/motioneye/translations/no.json
+++ b/homeassistant/components/motioneye/translations/no.json
@@ -11,6 +11,10 @@
"unknown": "Uventet feil"
},
"step": {
+ "hassio_confirm": {
+ "description": "Vil du konfigurere Home Assistant for \u00e5 koble til motionEye-tjenesten som tilbys av tillegget: {addon} ?",
+ "title": "motionEye via Home Assistant-tillegget"
+ },
"user": {
"data": {
"admin_password": "Admin Passord",
diff --git a/homeassistant/components/motioneye/translations/pl.json b/homeassistant/components/motioneye/translations/pl.json
index dca40bdcd3d..296c2f963af 100644
--- a/homeassistant/components/motioneye/translations/pl.json
+++ b/homeassistant/components/motioneye/translations/pl.json
@@ -11,6 +11,10 @@
"unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
+ "hassio_confirm": {
+ "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z motionEye przez dodatek {addon}?",
+ "title": "motionEye poprzez dodatek Home Assistant"
+ },
"user": {
"data": {
"admin_password": "Has\u0142o admina",
diff --git a/homeassistant/components/motioneye/translations/ru.json b/homeassistant/components/motioneye/translations/ru.json
index a983ddcae0f..8999b0e8f82 100644
--- a/homeassistant/components/motioneye/translations/ru.json
+++ b/homeassistant/components/motioneye/translations/ru.json
@@ -11,6 +11,10 @@
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
+ "hassio_confirm": {
+ "description": "\u041d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 motionEye (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?",
+ "title": "motionEye (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)"
+ },
"user": {
"data": {
"admin_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430",
diff --git a/homeassistant/components/motioneye/translations/zh-Hant.json b/homeassistant/components/motioneye/translations/zh-Hant.json
index aa05784e53d..8c143655f2f 100644
--- a/homeassistant/components/motioneye/translations/zh-Hant.json
+++ b/homeassistant/components/motioneye/translations/zh-Hant.json
@@ -11,6 +11,10 @@
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
+ "hassio_confirm": {
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 motionEye\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f",
+ "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 motionEye"
+ },
"user": {
"data": {
"admin_password": "Admin \u5bc6\u78bc",
diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py
index adb4bf0e810..baf57844eaf 100644
--- a/homeassistant/components/mpd/media_player.py
+++ b/homeassistant/components/mpd/media_player.py
@@ -167,7 +167,7 @@ class MpdDevice(MediaPlayerEntity):
self._commands = list(await self._client.commands())
await self._fetch_status()
- except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError) as error:
+ except (mpd.ConnectionError, OSError, ValueError) as error:
# Cleanly disconnect in case connection is not in valid state
_LOGGER.debug("Error updating status: %s", error)
self._disconnect()
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index de7fb69b8b6..3a6fd068975 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -220,7 +220,6 @@ MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend(
vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
- vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}
)
diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py
index 6c572c093a3..a2bd7fc6b36 100644
--- a/homeassistant/components/mqtt/abbreviations.py
+++ b/homeassistant/components/mqtt/abbreviations.py
@@ -92,6 +92,7 @@ ABBREVIATIONS = {
"name": "name",
"off_dly": "off_delay",
"on_cmd_type": "on_command_type",
+ "ops": "options",
"opt": "optimistic",
"osc_cmd_t": "oscillation_command_topic",
"osc_cmd_tpl": "oscillation_command_template",
@@ -220,6 +221,8 @@ ABBREVIATIONS = {
"uniq_id": "unique_id",
"unit_of_meas": "unit_of_measurement",
"val_tpl": "value_template",
+ "whit_cmd_t": "white_command_topic",
+ "whit_scl": "white_scale",
"whit_val_cmd_t": "white_value_command_topic",
"whit_val_scl": "white_value_scale",
"whit_val_stat_t": "white_value_state_topic",
diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py
index 1e7ccf5bb4c..aa98a48dc10 100644
--- a/homeassistant/components/mqtt/alarm_control_panel.py
+++ b/homeassistant/components/mqtt/alarm_control_panel.py
@@ -55,6 +55,14 @@ CONF_PAYLOAD_ARM_NIGHT = "payload_arm_night"
CONF_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass"
CONF_COMMAND_TEMPLATE = "command_template"
+MQTT_ALARM_ATTRIBUTES_BLOCKED = frozenset(
+ {
+ alarm.ATTR_CHANGED_BY,
+ alarm.ATTR_CODE_ARM_REQUIRED,
+ alarm.ATTR_CODE_FORMAT,
+ }
+)
+
DEFAULT_COMMAND_TEMPLATE = "{{action}}"
DEFAULT_ARM_NIGHT = "ARM_NIGHT"
DEFAULT_ARM_AWAY = "ARM_AWAY"
@@ -112,6 +120,8 @@ async def _async_setup_entity(
class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):
"""Representation of a MQTT alarm status."""
+ _attributes_extra_blocked = MQTT_ALARM_ATTRIBUTES_BLOCKED
+
def __init__(self, hass, config, config_entry, discovery_data):
"""Init the MQTT Alarm Control Panel."""
self._state = None
diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py
index 0a9f37ac9ea..adcb9ca623a 100644
--- a/homeassistant/components/mqtt/camera.py
+++ b/homeassistant/components/mqtt/camera.py
@@ -19,6 +19,15 @@ from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_hel
CONF_TOPIC = "topic"
DEFAULT_NAME = "MQTT Camera"
+MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset(
+ {
+ "access_token",
+ "brand",
+ "model_name",
+ "motion_detection",
+ }
+)
+
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@@ -32,33 +41,35 @@ async def async_setup_platform(
):
"""Set up MQTT camera through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
- await _async_setup_entity(async_add_entities, config)
+ await _async_setup_entity(hass, async_add_entities, config)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT camera dynamically through MQTT discovery."""
setup = functools.partial(
- _async_setup_entity, async_add_entities, config_entry=config_entry
+ _async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
await async_setup_entry_helper(hass, camera.DOMAIN, setup, PLATFORM_SCHEMA)
async def _async_setup_entity(
- async_add_entities, config, config_entry=None, discovery_data=None
+ hass, async_add_entities, config, config_entry=None, discovery_data=None
):
"""Set up the MQTT Camera."""
- async_add_entities([MqttCamera(config, config_entry, discovery_data)])
+ async_add_entities([MqttCamera(hass, config, config_entry, discovery_data)])
class MqttCamera(MqttEntity, Camera):
"""representation of a MQTT camera."""
- def __init__(self, config, config_entry, discovery_data):
+ _attributes_extra_blocked = MQTT_CAMERA_ATTRIBUTES_BLOCKED
+
+ def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT Camera."""
self._last_image = None
Camera.__init__(self)
- MqttEntity.__init__(self, None, config, config_entry, discovery_data)
+ MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@staticmethod
def config_schema():
diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py
index da0ed485b72..ccaa7c65176 100644
--- a/homeassistant/components/mqtt/climate.py
+++ b/homeassistant/components/mqtt/climate.py
@@ -119,6 +119,31 @@ CONF_TEMP_MAX = "max_temp"
CONF_TEMP_MIN = "min_temp"
CONF_TEMP_STEP = "temp_step"
+MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset(
+ {
+ climate.ATTR_AUX_HEAT,
+ climate.ATTR_CURRENT_HUMIDITY,
+ climate.ATTR_CURRENT_TEMPERATURE,
+ climate.ATTR_FAN_MODE,
+ climate.ATTR_FAN_MODES,
+ climate.ATTR_HUMIDITY,
+ climate.ATTR_HVAC_ACTION,
+ climate.ATTR_HVAC_MODES,
+ climate.ATTR_MAX_HUMIDITY,
+ climate.ATTR_MAX_TEMP,
+ climate.ATTR_MIN_HUMIDITY,
+ climate.ATTR_MIN_TEMP,
+ climate.ATTR_PRESET_MODE,
+ climate.ATTR_PRESET_MODES,
+ climate.ATTR_SWING_MODE,
+ climate.ATTR_SWING_MODES,
+ climate.ATTR_TARGET_TEMP_HIGH,
+ climate.ATTR_TARGET_TEMP_LOW,
+ climate.ATTR_TARGET_TEMP_STEP,
+ climate.ATTR_TEMPERATURE,
+ }
+)
+
VALUE_TEMPLATE_KEYS = (
CONF_AUX_STATE_TEMPLATE,
CONF_AWAY_MODE_STATE_TEMPLATE,
@@ -276,6 +301,8 @@ async def _async_setup_entity(
class MqttClimate(MqttEntity, ClimateEntity):
"""Representation of an MQTT climate device."""
+ _attributes_extra_blocked = MQTT_CLIMATE_ATTRIBUTES_BLOCKED
+
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the climate device."""
self._action = None
diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py
index 0a78a102a13..d920d12662f 100644
--- a/homeassistant/components/mqtt/cover.py
+++ b/homeassistant/components/mqtt/cover.py
@@ -101,6 +101,13 @@ TILT_FEATURES = (
| SUPPORT_SET_TILT_POSITION
)
+MQTT_COVER_ATTRIBUTES_BLOCKED = frozenset(
+ {
+ cover.ATTR_CURRENT_POSITION,
+ cover.ATTR_CURRENT_TILT_POSITION,
+ }
+)
+
def validate_options(value):
"""Validate options.
@@ -219,6 +226,8 @@ async def _async_setup_entity(
class MqttCover(MqttEntity, CoverEntity):
"""Representation of a cover that can be controlled using MQTT."""
+ _attributes_extra_blocked = MQTT_COVER_ATTRIBUTES_BLOCKED
+
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the cover."""
self._position = None
@@ -236,11 +245,45 @@ class MqttCover(MqttEntity, CoverEntity):
return PLATFORM_SCHEMA
def _setup_from_config(self, config):
- self._optimistic = config[CONF_OPTIMISTIC] or (
- config.get(CONF_STATE_TOPIC) is None
+ no_position = (
+ config.get(CONF_SET_POSITION_TOPIC) is None
and config.get(CONF_GET_POSITION_TOPIC) is None
)
- self._tilt_optimistic = config[CONF_TILT_STATE_OPTIMISTIC]
+ no_state = (
+ config.get(CONF_COMMAND_TOPIC) is None
+ and config.get(CONF_STATE_TOPIC) is None
+ )
+ no_tilt = (
+ config.get(CONF_TILT_COMMAND_TOPIC) is None
+ and config.get(CONF_TILT_STATUS_TOPIC) is None
+ )
+ optimistic_position = (
+ config.get(CONF_SET_POSITION_TOPIC) is not None
+ and config.get(CONF_GET_POSITION_TOPIC) is None
+ )
+ optimistic_state = (
+ config.get(CONF_COMMAND_TOPIC) is not None
+ and config.get(CONF_STATE_TOPIC) is None
+ )
+ optimistic_tilt = (
+ config.get(CONF_TILT_COMMAND_TOPIC) is not None
+ and config.get(CONF_TILT_STATUS_TOPIC) is None
+ )
+
+ if config[CONF_OPTIMISTIC] or (
+ (no_position or optimistic_position)
+ and (no_state or optimistic_state)
+ and (no_tilt or optimistic_tilt)
+ ):
+ # Force into optimistic mode.
+ self._optimistic = True
+
+ if (
+ config[CONF_TILT_STATE_OPTIMISTIC]
+ or config.get(CONF_TILT_STATUS_TOPIC) is None
+ ):
+ # Force into optimistic tilt mode.
+ self._tilt_optimistic = True
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
@@ -409,17 +452,7 @@ class MqttCover(MqttEntity, CoverEntity):
"qos": self._config[CONF_QOS],
}
- if (
- self._config.get(CONF_GET_POSITION_TOPIC) is None
- and self._config.get(CONF_STATE_TOPIC) is None
- ):
- # Force into optimistic mode.
- self._optimistic = True
-
- if self._config.get(CONF_TILT_STATUS_TOPIC) is None:
- # Force into optimistic tilt mode.
- self._tilt_optimistic = True
- else:
+ if self._config.get(CONF_TILT_STATUS_TOPIC) is not None:
self._tilt_value = STATE_UNKNOWN
topics["tilt_status_topic"] = {
"topic": self._config.get(CONF_TILT_STATUS_TOPIC),
diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py
index 038e6e91523..d9413b80c06 100644
--- a/homeassistant/components/mqtt/device_trigger.py
+++ b/homeassistant/components/mqtt/device_trigger.py
@@ -8,7 +8,7 @@ import attr
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.const import (
CONF_DEVICE,
CONF_DEVICE_ID,
@@ -54,7 +54,7 @@ MQTT_TRIGGER_BASE = {
CONF_DOMAIN: DOMAIN,
}
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): DEVICE,
vol.Required(CONF_DOMAIN): DOMAIN,
diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py
index 3a5a3cb5f87..d35065e30a8 100644
--- a/homeassistant/components/mqtt/discovery.py
+++ b/homeassistant/components/mqtt/discovery.py
@@ -9,6 +9,7 @@ import time
from homeassistant.const import CONF_DEVICE, CONF_PLATFORM
from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import RESULT_TYPE_ABORT
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
@@ -44,6 +45,7 @@ SUPPORTED_COMPONENTS = [
"lock",
"number",
"scene",
+ "select",
"sensor",
"switch",
"tag",
@@ -94,6 +96,10 @@ async def async_start( # noqa: C901
match = TOPIC_MATCHER.match(topic_trimmed)
if not match:
+ if topic_trimmed.endswith("config"):
+ _LOGGER.warning(
+ "Received message on illegal discovery topic '%s'", topic
+ )
return
component, node_id, object_id = match.groups()
@@ -274,7 +280,7 @@ async def async_start( # noqa: C901
)
if (
result
- and result["type"] == "abort"
+ and result["type"] == RESULT_TYPE_ABORT
and result["reason"]
in ["already_configured", "single_instance_allowed"]
):
diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py
index 5cd924551f7..ef996a3c4ba 100644
--- a/homeassistant/components/mqtt/fan.py
+++ b/homeassistant/components/mqtt/fan.py
@@ -94,6 +94,18 @@ DEFAULT_SPEED_RANGE_MAX = 100
OSCILLATE_ON_PAYLOAD = "oscillate_on"
OSCILLATE_OFF_PAYLOAD = "oscillate_off"
+MQTT_FAN_ATTRIBUTES_BLOCKED = frozenset(
+ {
+ fan.ATTR_DIRECTION,
+ fan.ATTR_OSCILLATING,
+ fan.ATTR_PERCENTAGE_STEP,
+ fan.ATTR_PERCENTAGE,
+ fan.ATTR_PRESET_MODE,
+ fan.ATTR_PRESET_MODES,
+ fan.ATTR_SPEED_LIST,
+ fan.ATTR_SPEED,
+ }
+)
_LOGGER = logging.getLogger(__name__)
@@ -124,14 +136,14 @@ def valid_preset_mode_configuration(config):
PLATFORM_SCHEMA = vol.All(
- # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_STATE_VALUE_TEMPLATE, CONF_SPEED_LIST and
+ # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LIST, CONF_SPEED_STATE_TOPIC, CONF_SPEED_VALUE_TEMPLATE and
# Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF,
# are deprecated, support will be removed after a quarter (2021.7)
cv.deprecated(CONF_PAYLOAD_HIGH_SPEED),
cv.deprecated(CONF_PAYLOAD_LOW_SPEED),
cv.deprecated(CONF_PAYLOAD_MEDIUM_SPEED),
- cv.deprecated(CONF_SPEED_LIST),
cv.deprecated(CONF_SPEED_COMMAND_TOPIC),
+ cv.deprecated(CONF_SPEED_LIST),
cv.deprecated(CONF_SPEED_STATE_TOPIC),
cv.deprecated(CONF_SPEED_VALUE_TEMPLATE),
mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
@@ -223,6 +235,8 @@ async def _async_setup_entity(
class MqttFan(MqttEntity, FanEntity):
"""A MQTT fan component."""
+ _attributes_extra_blocked = MQTT_FAN_ATTRIBUTES_BLOCKED
+
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT fan."""
self._state = False
diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py
index 684dcf337aa..c4af68d3044 100644
--- a/homeassistant/components/mqtt/light/schema_basic.py
+++ b/homeassistant/components/mqtt/light/schema_basic.py
@@ -8,10 +8,15 @@ from homeassistant.components.light import (
ATTR_COLOR_MODE,
ATTR_COLOR_TEMP,
ATTR_EFFECT,
+ ATTR_EFFECT_LIST,
ATTR_HS_COLOR,
+ ATTR_MAX_MIREDS,
+ ATTR_MIN_MIREDS,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
+ ATTR_SUPPORTED_COLOR_MODES,
+ ATTR_WHITE,
ATTR_WHITE_VALUE,
ATTR_XY_COLOR,
COLOR_MODE_BRIGHTNESS,
@@ -22,6 +27,7 @@ from homeassistant.components.light import (
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
COLOR_MODE_UNKNOWN,
+ COLOR_MODE_WHITE,
COLOR_MODE_XY,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
@@ -29,6 +35,7 @@ from homeassistant.components.light import (
SUPPORT_EFFECT,
SUPPORT_WHITE_VALUE,
LightEntity,
+ valid_supported_color_modes,
)
from homeassistant.const import (
CONF_NAME,
@@ -86,18 +93,40 @@ CONF_STATE_VALUE_TEMPLATE = "state_value_template"
CONF_XY_COMMAND_TOPIC = "xy_command_topic"
CONF_XY_STATE_TOPIC = "xy_state_topic"
CONF_XY_VALUE_TEMPLATE = "xy_value_template"
+CONF_WHITE_COMMAND_TOPIC = "white_command_topic"
+CONF_WHITE_SCALE = "white_scale"
CONF_WHITE_VALUE_COMMAND_TOPIC = "white_value_command_topic"
CONF_WHITE_VALUE_SCALE = "white_value_scale"
CONF_WHITE_VALUE_STATE_TOPIC = "white_value_state_topic"
CONF_WHITE_VALUE_TEMPLATE = "white_value_template"
CONF_ON_COMMAND_TYPE = "on_command_type"
+MQTT_LIGHT_ATTRIBUTES_BLOCKED = frozenset(
+ {
+ ATTR_COLOR_MODE,
+ ATTR_BRIGHTNESS,
+ ATTR_COLOR_TEMP,
+ ATTR_EFFECT,
+ ATTR_EFFECT_LIST,
+ ATTR_HS_COLOR,
+ ATTR_MAX_MIREDS,
+ ATTR_MIN_MIREDS,
+ ATTR_RGB_COLOR,
+ ATTR_RGBW_COLOR,
+ ATTR_RGBWW_COLOR,
+ ATTR_SUPPORTED_COLOR_MODES,
+ ATTR_WHITE_VALUE,
+ ATTR_XY_COLOR,
+ }
+)
+
DEFAULT_BRIGHTNESS_SCALE = 255
DEFAULT_NAME = "MQTT LightEntity"
DEFAULT_OPTIMISTIC = False
DEFAULT_PAYLOAD_OFF = "OFF"
DEFAULT_PAYLOAD_ON = "ON"
DEFAULT_WHITE_VALUE_SCALE = 255
+DEFAULT_WHITE_SCALE = 255
DEFAULT_ON_COMMAND_TYPE = "last"
VALUES_ON_COMMAND_TYPE = ["first", "last", "brightness"]
@@ -122,7 +151,9 @@ VALUE_TEMPLATE_KEYS = [
CONF_XY_VALUE_TEMPLATE,
]
-PLATFORM_SCHEMA_BASIC = (
+PLATFORM_SCHEMA_BASIC = vol.All(
+ # CONF_VALUE_TEMPLATE is deprecated, support will be removed in 2021.10
+ cv.deprecated(CONF_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE),
mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic,
@@ -166,6 +197,11 @@ PLATFORM_SCHEMA_BASIC = (
vol.Optional(CONF_RGBWW_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_RGBWW_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
+ vol.Optional(CONF_WHITE_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All(
+ vol.Coerce(int), vol.Range(min=1)
+ ),
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(
CONF_WHITE_VALUE_SCALE, default=DEFAULT_WHITE_VALUE_SCALE
@@ -178,7 +214,7 @@ PLATFORM_SCHEMA_BASIC = (
}
)
.extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
- .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema)
+ .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema),
)
@@ -192,6 +228,8 @@ async def async_setup_entity_basic(
class MqttLight(MqttEntity, LightEntity, RestoreEntity):
"""Representation of a MQTT light."""
+ _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED
+
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize MQTT light."""
self._brightness = None
@@ -256,6 +294,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
CONF_RGBWW_COMMAND_TOPIC,
CONF_RGBWW_STATE_TOPIC,
CONF_STATE_TOPIC,
+ CONF_WHITE_COMMAND_TOPIC,
CONF_WHITE_VALUE_COMMAND_TOPIC,
CONF_WHITE_VALUE_STATE_TOPIC,
CONF_XY_COMMAND_TOPIC,
@@ -313,35 +352,40 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None
)
self._optimistic_xy_color = optimistic or topic[CONF_XY_STATE_TOPIC] is None
- self._supported_color_modes = set()
+ supported_color_modes = set()
if topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None:
- self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
+ supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
self._color_mode = COLOR_MODE_COLOR_TEMP
if topic[CONF_HS_COMMAND_TOPIC] is not None:
- self._supported_color_modes.add(COLOR_MODE_HS)
+ supported_color_modes.add(COLOR_MODE_HS)
self._color_mode = COLOR_MODE_HS
if topic[CONF_RGB_COMMAND_TOPIC] is not None:
- self._supported_color_modes.add(COLOR_MODE_RGB)
+ supported_color_modes.add(COLOR_MODE_RGB)
self._color_mode = COLOR_MODE_RGB
if topic[CONF_RGBW_COMMAND_TOPIC] is not None:
- self._supported_color_modes.add(COLOR_MODE_RGBW)
+ supported_color_modes.add(COLOR_MODE_RGBW)
self._color_mode = COLOR_MODE_RGBW
if topic[CONF_RGBWW_COMMAND_TOPIC] is not None:
- self._supported_color_modes.add(COLOR_MODE_RGBWW)
+ supported_color_modes.add(COLOR_MODE_RGBWW)
self._color_mode = COLOR_MODE_RGBWW
+ if topic[CONF_WHITE_COMMAND_TOPIC] is not None:
+ supported_color_modes.add(COLOR_MODE_WHITE)
if topic[CONF_XY_COMMAND_TOPIC] is not None:
- self._supported_color_modes.add(COLOR_MODE_XY)
+ supported_color_modes.add(COLOR_MODE_XY)
self._color_mode = COLOR_MODE_XY
- if len(self._supported_color_modes) > 1:
+ if len(supported_color_modes) > 1:
self._color_mode = COLOR_MODE_UNKNOWN
- if not self._supported_color_modes:
+ if not supported_color_modes:
if topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None:
self._color_mode = COLOR_MODE_BRIGHTNESS
- self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
+ supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
else:
self._color_mode = COLOR_MODE_ONOFF
- self._supported_color_modes.add(COLOR_MODE_ONOFF)
+ supported_color_modes.add(COLOR_MODE_ONOFF)
+
+ # Validate the color_modes configuration
+ self._supported_color_modes = valid_supported_color_modes(supported_color_modes)
if topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None:
self._legacy_mode = True
@@ -814,7 +858,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
# If brightness is being used instead of an on command, make sure
# there is a brightness input. Either set the brightness to our
# saved value or the maximum value if this is the first call
- elif on_command_type == "brightness" and ATTR_BRIGHTNESS not in kwargs:
+ elif (
+ on_command_type == "brightness"
+ and ATTR_BRIGHTNESS not in kwargs
+ and ATTR_WHITE not in kwargs
+ ):
kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255
hs_color = kwargs.get(ATTR_HS_COLOR)
@@ -968,6 +1016,17 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
publish(CONF_EFFECT_COMMAND_TOPIC, effect)
should_update |= set_optimistic(ATTR_EFFECT, effect)
+ if ATTR_WHITE in kwargs and self._topic[CONF_WHITE_COMMAND_TOPIC] is not None:
+ percent_white = float(kwargs[ATTR_WHITE]) / 255
+ white_scale = self._config[CONF_WHITE_SCALE]
+ device_white_value = min(round(percent_white * white_scale), white_scale)
+ publish(CONF_WHITE_COMMAND_TOPIC, device_white_value)
+ should_update |= set_optimistic(
+ ATTR_BRIGHTNESS,
+ kwargs[ATTR_WHITE],
+ COLOR_MODE_WHITE,
+ )
+
if (
ATTR_WHITE_VALUE in kwargs
and self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None
diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py
index 5143b92622a..1223b79148a 100644
--- a/homeassistant/components/mqtt/light/schema_json.py
+++ b/homeassistant/components/mqtt/light/schema_json.py
@@ -61,7 +61,7 @@ from ... import mqtt
from ..debug_info import log_messages
from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity
from .schema import MQTT_LIGHT_SCHEMA_SCHEMA
-from .schema_basic import CONF_BRIGHTNESS_SCALE
+from .schema_basic import CONF_BRIGHTNESS_SCALE, MQTT_LIGHT_ATTRIBUTES_BLOCKED
_LOGGER = logging.getLogger(__name__)
@@ -151,13 +151,15 @@ async def async_setup_entity_json(
hass, config: ConfigType, async_add_entities, config_entry, discovery_data
):
"""Set up a MQTT JSON Light."""
- async_add_entities([MqttLightJson(config, config_entry, discovery_data)])
+ async_add_entities([MqttLightJson(hass, config, config_entry, discovery_data)])
class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
"""Representation of a MQTT JSON light."""
- def __init__(self, config, config_entry, discovery_data):
+ _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED
+
+ def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize MQTT JSON light."""
self._state = False
self._supported_features = 0
@@ -176,7 +178,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
self._white_value = None
self._xy = None
- MqttEntity.__init__(self, None, config, config_entry, discovery_data)
+ MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@staticmethod
def config_schema():
diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py
index c5eee7006d6..9a23d9ea2f9 100644
--- a/homeassistant/components/mqtt/light/schema_template.py
+++ b/homeassistant/components/mqtt/light/schema_template.py
@@ -37,6 +37,7 @@ from ... import mqtt
from ..debug_info import log_messages
from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity
from .schema import MQTT_LIGHT_SCHEMA_SCHEMA
+from .schema_basic import MQTT_LIGHT_ATTRIBUTES_BLOCKED
_LOGGER = logging.getLogger(__name__)
@@ -87,13 +88,15 @@ async def async_setup_entity_template(
hass, config, async_add_entities, config_entry, discovery_data
):
"""Set up a MQTT Template light."""
- async_add_entities([MqttLightTemplate(config, config_entry, discovery_data)])
+ async_add_entities([MqttLightTemplate(hass, config, config_entry, discovery_data)])
class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
"""Representation of a MQTT Template light."""
- def __init__(self, config, config_entry, discovery_data):
+ _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED
+
+ def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize a MQTT Template light."""
self._state = False
@@ -108,7 +111,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
self._hs = None
self._effect = None
- MqttEntity.__init__(self, None, config, config_entry, discovery_data)
+ MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@staticmethod
def config_schema():
diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py
index 24d58b148fa..45703fc0cc3 100644
--- a/homeassistant/components/mqtt/lock.py
+++ b/homeassistant/components/mqtt/lock.py
@@ -37,6 +37,13 @@ DEFAULT_PAYLOAD_UNLOCK = "UNLOCK"
DEFAULT_STATE_LOCKED = "LOCKED"
DEFAULT_STATE_UNLOCKED = "UNLOCKED"
+MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset(
+ {
+ lock.ATTR_CHANGED_BY,
+ lock.ATTR_CODE_FORMAT,
+ }
+)
+
PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@@ -45,6 +52,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string,
vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string,
vol.Optional(CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED): cv.string,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
@@ -75,6 +83,8 @@ async def _async_setup_entity(
class MqttLock(MqttEntity, LockEntity):
"""Representation of a lock that can be toggled using MQTT."""
+ _attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED
+
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the lock."""
self._state = False
diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py
index 9b1c7a9fb21..b0c8b573b37 100644
--- a/homeassistant/components/mqtt/mixins.py
+++ b/homeassistant/components/mqtt/mixins.py
@@ -67,6 +67,25 @@ CONF_VIA_DEVICE = "via_device"
CONF_DEPRECATED_VIA_HUB = "via_hub"
CONF_SUGGESTED_AREA = "suggested_area"
+MQTT_ATTRIBUTES_BLOCKED = {
+ "assumed_state",
+ "available",
+ "context_recent_time",
+ "device_class",
+ "device_info",
+ "entity_picture",
+ "entity_registry_enabled_default",
+ "extra_state_attributes",
+ "force_update",
+ "icon",
+ "name",
+ "should_poll",
+ "state",
+ "supported_features",
+ "unique_id",
+ "unit_of_measurement",
+}
+
MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema(
{
vol.Exclusive(CONF_AVAILABILITY_TOPIC, "availability"): valid_subscribe_topic,
@@ -175,6 +194,8 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema):
class MqttAttributes(Entity):
"""Mixin used for platforms that support JSON attributes."""
+ _attributes_extra_blocked = frozenset()
+
def __init__(self, config: dict) -> None:
"""Initialize the JSON attributes mixin."""
self._attributes = None
@@ -206,7 +227,13 @@ class MqttAttributes(Entity):
payload = attr_tpl.async_render_with_possible_json_value(payload)
json_dict = json.loads(payload)
if isinstance(json_dict, dict):
- self._attributes = json_dict
+ filtered_dict = {
+ k: v
+ for k, v in json_dict.items()
+ if k not in MQTT_ATTRIBUTES_BLOCKED
+ and k not in self._attributes_extra_blocked
+ }
+ self._attributes = filtered_dict
self.async_write_ha_state()
else:
_LOGGER.warning("JSON result was not a dictionary")
diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py
index 95409924fa4..866f93fd674 100644
--- a/homeassistant/components/mqtt/number.py
+++ b/homeassistant/components/mqtt/number.py
@@ -11,7 +11,7 @@ from homeassistant.components.number import (
DEFAULT_STEP,
NumberEntity,
)
-from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC
+from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service
@@ -40,11 +40,19 @@ CONF_STEP = "step"
DEFAULT_NAME = "MQTT Number"
DEFAULT_OPTIMISTIC = False
+MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset(
+ {
+ number.ATTR_MAX,
+ number.ATTR_MIN,
+ number.ATTR_STEP,
+ }
+)
+
def validate_config(config):
"""Validate that the configuration is valid, throws if it isn't."""
if config.get(CONF_MIN) >= config.get(CONF_MAX):
- raise vol.Invalid(f"'{CONF_MAX}'' must be > '{CONF_MIN}'")
+ raise vol.Invalid(f"'{CONF_MAX}' must be > '{CONF_MIN}'")
return config
@@ -59,6 +67,7 @@ PLATFORM_SCHEMA = vol.All(
vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All(
vol.Coerce(float), vol.Range(min=1e-3)
),
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
},
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema),
validate_config,
@@ -70,43 +79,53 @@ async def async_setup_platform(
):
"""Set up MQTT number through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
- await _async_setup_entity(async_add_entities, config)
+ await _async_setup_entity(hass, async_add_entities, config)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT number dynamically through MQTT discovery."""
setup = functools.partial(
- _async_setup_entity, async_add_entities, config_entry=config_entry
+ _async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
await async_setup_entry_helper(hass, number.DOMAIN, setup, PLATFORM_SCHEMA)
async def _async_setup_entity(
- async_add_entities, config, config_entry=None, discovery_data=None
+ hass, async_add_entities, config, config_entry=None, discovery_data=None
):
"""Set up the MQTT number."""
- async_add_entities([MqttNumber(config, config_entry, discovery_data)])
+ async_add_entities([MqttNumber(hass, config, config_entry, discovery_data)])
class MqttNumber(MqttEntity, NumberEntity, RestoreEntity):
"""representation of an MQTT number."""
- def __init__(self, config, config_entry, discovery_data):
+ _attributes_extra_blocked = MQTT_NUMBER_ATTRIBUTES_BLOCKED
+
+ def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT Number."""
self._config = config
+ self._optimistic = False
self._sub_state = None
self._current_number = None
- self._optimistic = config.get(CONF_OPTIMISTIC)
NumberEntity.__init__(self)
- MqttEntity.__init__(self, None, config, config_entry, discovery_data)
+ MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@staticmethod
def config_schema():
"""Return the config schema."""
return PLATFORM_SCHEMA
+ def _setup_from_config(self, config):
+ """(Re)Setup the entity."""
+ self._optimistic = config[CONF_OPTIMISTIC]
+
+ value_template = self._config.get(CONF_VALUE_TEMPLATE)
+ if value_template is not None:
+ value_template.hass = self.hass
+
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@@ -114,16 +133,17 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity):
@log_messages(self.hass, self.entity_id)
def message_received(msg):
"""Handle new MQTT messages."""
+ payload = msg.payload
+ value_template = self._config.get(CONF_VALUE_TEMPLATE)
+ if value_template is not None:
+ payload = value_template.async_render_with_possible_json_value(payload)
try:
- if msg.payload.decode("utf-8").isnumeric():
- num_value = int(msg.payload)
+ if payload.isnumeric():
+ num_value = int(payload)
else:
- num_value = float(msg.payload)
+ num_value = float(payload)
except ValueError:
- _LOGGER.warning(
- "Payload '%s' is not a Number",
- msg.payload.decode("utf-8", errors="ignore"),
- )
+ _LOGGER.warning("Payload '%s' is not a Number", msg.payload)
return
if num_value < self.min_value or num_value > self.max_value:
@@ -151,7 +171,6 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity):
"topic": self._config.get(CONF_STATE_TOPIC),
"msg_callback": message_received,
"qos": self._config[CONF_QOS],
- "encoding": None,
}
},
)
diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py
new file mode 100644
index 00000000000..98643917788
--- /dev/null
+++ b/homeassistant/components/mqtt/select.py
@@ -0,0 +1,179 @@
+"""Configure select in a device through MQTT topic."""
+import functools
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components import select
+from homeassistant.components.select import SelectEntity
+from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.reload import async_setup_reload_service
+from homeassistant.helpers.restore_state import RestoreEntity
+from homeassistant.helpers.typing import ConfigType
+
+from . import (
+ CONF_COMMAND_TOPIC,
+ CONF_QOS,
+ CONF_STATE_TOPIC,
+ DOMAIN,
+ PLATFORMS,
+ subscription,
+)
+from .. import mqtt
+from .const import CONF_RETAIN
+from .debug_info import log_messages
+from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_OPTIONS = "options"
+
+DEFAULT_NAME = "MQTT Select"
+DEFAULT_OPTIMISTIC = False
+
+MQTT_SELECT_ATTRIBUTES_BLOCKED = frozenset(
+ {
+ select.ATTR_OPTIONS,
+ }
+)
+
+
+def validate_config(config):
+ """Validate that the configuration is valid, throws if it isn't."""
+ if len(config[CONF_OPTIONS]) < 2:
+ raise vol.Invalid(f"'{CONF_OPTIONS}' must include at least 2 options")
+
+ return config
+
+
+PLATFORM_SCHEMA = vol.All(
+ mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
+ vol.Required(CONF_OPTIONS): cv.ensure_list,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
+ },
+ ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema),
+ validate_config,
+)
+
+
+async def async_setup_platform(
+ hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
+):
+ """Set up MQTT select through configuration.yaml."""
+ await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
+ await _async_setup_entity(hass, async_add_entities, config)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up MQTT select dynamically through MQTT discovery."""
+ setup = functools.partial(
+ _async_setup_entity, hass, async_add_entities, config_entry=config_entry
+ )
+ await async_setup_entry_helper(hass, select.DOMAIN, setup, PLATFORM_SCHEMA)
+
+
+async def _async_setup_entity(
+ hass, async_add_entities, config, config_entry=None, discovery_data=None
+):
+ """Set up the MQTT select."""
+ async_add_entities([MqttSelect(hass, config, config_entry, discovery_data)])
+
+
+class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
+ """representation of an MQTT select."""
+
+ _attributes_extra_blocked = MQTT_SELECT_ATTRIBUTES_BLOCKED
+
+ def __init__(self, hass, config, config_entry, discovery_data):
+ """Initialize the MQTT select."""
+ self._config = config
+ self._optimistic = False
+ self._sub_state = None
+
+ self._attr_current_option = None
+
+ SelectEntity.__init__(self)
+ MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
+
+ @staticmethod
+ def config_schema():
+ """Return the config schema."""
+ return PLATFORM_SCHEMA
+
+ def _setup_from_config(self, config):
+ """(Re)Setup the entity."""
+ self._optimistic = config[CONF_OPTIMISTIC]
+ self._attr_options = config[CONF_OPTIONS]
+
+ value_template = self._config.get(CONF_VALUE_TEMPLATE)
+ if value_template is not None:
+ value_template.hass = self.hass
+
+ async def _subscribe_topics(self):
+ """(Re)Subscribe to topics."""
+
+ @callback
+ @log_messages(self.hass, self.entity_id)
+ def message_received(msg):
+ """Handle new MQTT messages."""
+ payload = msg.payload
+ value_template = self._config.get(CONF_VALUE_TEMPLATE)
+ if value_template is not None:
+ payload = value_template.async_render_with_possible_json_value(payload)
+
+ if payload not in self.options:
+ _LOGGER.error(
+ "Invalid option for %s: '%s' (valid options: %s)",
+ self.entity_id,
+ payload,
+ self.options,
+ )
+ return
+
+ self._attr_current_option = payload
+ self.async_write_ha_state()
+
+ if self._config.get(CONF_STATE_TOPIC) is None:
+ # Force into optimistic mode.
+ self._optimistic = True
+ else:
+ self._sub_state = await subscription.async_subscribe_topics(
+ self.hass,
+ self._sub_state,
+ {
+ "state_topic": {
+ "topic": self._config.get(CONF_STATE_TOPIC),
+ "msg_callback": message_received,
+ "qos": self._config[CONF_QOS],
+ }
+ },
+ )
+
+ if self._optimistic:
+ last_state = await self.async_get_last_state()
+ if last_state:
+ self._attr_current_option = last_state.state
+
+ async def async_select_option(self, option: str) -> None:
+ """Update the current value."""
+ if self._optimistic:
+ self._attr_current_option = option
+ self.async_write_ha_state()
+
+ mqtt.async_publish(
+ self.hass,
+ self._config[CONF_COMMAND_TOPIC],
+ option,
+ self._config[CONF_QOS],
+ self._config[CONF_RETAIN],
+ )
+
+ @property
+ def assumed_state(self):
+ """Return true if we do optimistic updates."""
+ return self._optimistic
diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py
index 51caeb5f6da..777a15b639a 100644
--- a/homeassistant/components/mqtt/sensor.py
+++ b/homeassistant/components/mqtt/sensor.py
@@ -44,6 +44,13 @@ CONF_LAST_RESET_TOPIC = "last_reset_topic"
CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"
CONF_STATE_CLASS = "state_class"
+MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset(
+ {
+ sensor.ATTR_LAST_RESET,
+ sensor.ATTR_STATE_CLASS,
+ }
+)
+
DEFAULT_NAME = "MQTT Sensor"
DEFAULT_FORCE_UPDATE = False
PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend(
@@ -87,6 +94,7 @@ class MqttSensor(MqttEntity, SensorEntity):
"""Representation of a sensor that can be updated using MQTT."""
_attr_last_reset = None
+ _attributes_extra_blocked = MQTT_SENSOR_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the sensor."""
diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py
index d07f639f41d..f383a2ae310 100644
--- a/homeassistant/components/mqtt/switch.py
+++ b/homeassistant/components/mqtt/switch.py
@@ -32,6 +32,13 @@ from .. import mqtt
from .debug_info import log_messages
from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_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"
@@ -47,6 +54,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_STATE_OFF): cv.string,
vol.Optional(CONF_STATE_ON): cv.string,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
@@ -77,6 +85,8 @@ async def _async_setup_entity(
class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity):
"""Representation of a switch that can be toggled using MQTT."""
+ _attributes_extra_blocked = MQTT_SWITCH_ATTRIBUTES_BLOCKED
+
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT switch."""
self._state = False
diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json
index ffd2d1b36a1..132b4c42e18 100644
--- a/homeassistant/components/mqtt/translations/de.json
+++ b/homeassistant/components/mqtt/translations/de.json
@@ -62,13 +62,22 @@
"port": "Port",
"username": "Benutzername"
},
- "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein."
+ "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein.",
+ "title": "Broker-Optionen"
},
"options": {
"data": {
+ "birth_enable": "Birth Nachricht aktivieren",
"birth_payload": "Nutzdaten der Birth Nachricht",
+ "birth_qos": "Birth Nachricht QoS",
+ "birth_retain": "Birth Nachricht zwischenspeichern",
+ "birth_topic": "Thema der Birth Nachricht",
"discovery": "Erkennung aktivieren",
- "will_enable": "Letzten Willen aktivieren"
+ "will_enable": "Letzten Willen aktivieren",
+ "will_payload": "Nutzdaten der Letzter-Wille Nachricht",
+ "will_qos": "Letzter-Wille Nachricht QoS",
+ "will_retain": "Letzter-Wille Nachricht zwischenspeichern",
+ "will_topic": "Thema der Letzter-Wille Nachricht"
},
"description": "Erkennung - Wenn die Erkennung aktiviert ist (empfohlen), erkennt Home Assistant automatisch Ger\u00e4te und Entit\u00e4ten, die ihre Konfiguration auf dem MQTT-Broker ver\u00f6ffentlichen. Wenn die Erkennung deaktiviert ist, muss die gesamte Konfiguration manuell vorgenommen werden.\nGeburtsnachricht - Die Geburtsnachricht wird jedes Mal gesendet, wenn sich Home Assistant (erneut) mit dem MQTT-Broker verbindet.\nWill-Nachricht - Die Will-Nachricht wird jedes Mal gesendet, wenn Home Assistant die Verbindung zum Broker verliert, sowohl im Falle einer sauberen (z. B. Herunterfahren von Home Assistant) als auch im Falle einer unsauberen (z. B. Absturz von Home Assistant oder Verlust der Netzwerkverbindung) Verbindungstrennung.",
"title": "MQTT-Optionen"
diff --git a/homeassistant/components/mqtt/translations/he.json b/homeassistant/components/mqtt/translations/he.json
index bd083dfc1ec..ef628fb799b 100644
--- a/homeassistant/components/mqtt/translations/he.json
+++ b/homeassistant/components/mqtt/translations/he.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc MQTT \u05de\u05d5\u05ea\u05e8\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."
},
"error": {
- "cannot_connect": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d1\u05e8\u05d5\u05e7\u05e8."
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
},
"step": {
"broker": {
@@ -12,11 +12,29 @@
"broker": "\u05d1\u05e8\u05d5\u05e7\u05e8",
"discovery": "\u05d0\u05e4\u05e9\u05e8 \u05d2\u05d9\u05dc\u05d5\u05d9",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
- "port": "\u05e4\u05d5\u05e8\u05d8",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
},
"description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05d4\u05d1\u05e8\u05d5\u05e7\u05e8 MQTT \u05e9\u05dc\u05da."
}
}
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ },
+ "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05ea\u05d5\u05d5\u05da"
+ },
+ "options": {
+ "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea MQTT"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py
index 34c47aec791..3ee23356c3f 100644
--- a/homeassistant/components/mqtt/trigger.py
+++ b/homeassistant/components/mqtt/trigger.py
@@ -19,7 +19,7 @@ CONF_TOPIC = "topic"
DEFAULT_ENCODING = "utf-8"
DEFAULT_QOS = 0
-TRIGGER_SCHEMA = vol.Schema(
+TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): mqtt.DOMAIN,
vol.Required(CONF_TOPIC): mqtt.util.valid_subscribe_topic_template,
@@ -37,7 +37,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
topic = config[CONF_TOPIC]
wanted_payload = config.get(CONF_PAYLOAD)
value_template = config.get(CONF_VALUE_TEMPLATE)
@@ -74,12 +74,12 @@ async def async_attach_trigger(hass, config, action, automation_info):
if wanted_payload is None or wanted_payload == payload:
data = {
+ **trigger_data,
"platform": "mqtt",
"topic": mqttmsg.topic,
"payload": mqttmsg.payload,
"qos": mqttmsg.qos,
"description": f"mqtt topic {mqttmsg.topic}",
- "id": trigger_id,
}
with suppress(ValueError):
diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py
index 85fd1247381..12d2ff5319c 100644
--- a/homeassistant/components/mqtt/vacuum/__init__.py
+++ b/homeassistant/components/mqtt/vacuum/__init__.py
@@ -27,22 +27,22 @@ PLATFORM_SCHEMA = vol.All(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up MQTT vacuum through configuration.yaml."""
await async_setup_reload_service(hass, MQTT_DOMAIN, PLATFORMS)
- await _async_setup_entity(async_add_entities, config)
+ await _async_setup_entity(hass, async_add_entities, config)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT vacuum dynamically through MQTT discovery."""
setup = functools.partial(
- _async_setup_entity, async_add_entities, config_entry=config_entry
+ _async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
await async_setup_entry_helper(hass, DOMAIN, setup, PLATFORM_SCHEMA)
async def _async_setup_entity(
- async_add_entities, config, config_entry=None, discovery_data=None
+ hass, async_add_entities, config, config_entry=None, discovery_data=None
):
"""Set up the MQTT vacuum."""
setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state}
await setup_entity[config[CONF_SCHEMA]](
- config, async_add_entities, config_entry, discovery_data
+ hass, config, async_add_entities, config_entry, discovery_data
)
diff --git a/homeassistant/components/mqtt/vacuum/const.py b/homeassistant/components/mqtt/vacuum/const.py
new file mode 100644
index 00000000000..26e11125556
--- /dev/null
+++ b/homeassistant/components/mqtt/vacuum/const.py
@@ -0,0 +1,10 @@
+"""Shared constants."""
+from homeassistant.components import vacuum
+
+MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset(
+ {
+ vacuum.ATTR_BATTERY_ICON,
+ vacuum.ATTR_BATTERY_LEVEL,
+ vacuum.ATTR_FAN_SPEED,
+ }
+)
diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py
index f0f00a72bb4..009cfe18016 100644
--- a/homeassistant/components/mqtt/vacuum/schema_legacy.py
+++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py
@@ -4,6 +4,7 @@ import json
import voluptuous as vol
from homeassistant.components.vacuum import (
+ ATTR_STATUS,
SUPPORT_BATTERY,
SUPPORT_CLEAN_SPOT,
SUPPORT_FAN_SPEED,
@@ -26,6 +27,7 @@ from .. import subscription
from ... import mqtt
from ..debug_info import log_messages
from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity
+from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED
from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services
SERVICE_TO_STRING = {
@@ -96,6 +98,10 @@ DEFAULT_PAYLOAD_TURN_ON = "turn_on"
DEFAULT_RETAIN = False
DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES, SERVICE_TO_STRING)
+MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED = MQTT_VACUUM_ATTRIBUTES_BLOCKED | frozenset(
+ {ATTR_STATUS}
+)
+
PLATFORM_SCHEMA_LEGACY = (
mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
{
@@ -151,16 +157,18 @@ PLATFORM_SCHEMA_LEGACY = (
async def async_setup_entity_legacy(
- config, async_add_entities, config_entry, discovery_data
+ hass, config, async_add_entities, config_entry, discovery_data
):
"""Set up a MQTT Vacuum Legacy."""
- async_add_entities([MqttVacuum(config, config_entry, discovery_data)])
+ async_add_entities([MqttVacuum(hass, config, config_entry, discovery_data)])
class MqttVacuum(MqttEntity, VacuumEntity):
"""Representation of a MQTT-controlled legacy vacuum."""
- def __init__(self, config, config_entry, discovery_data):
+ _attributes_extra_blocked = MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED
+
+ def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the vacuum."""
self._cleaning = False
self._charging = False
@@ -171,7 +179,7 @@ class MqttVacuum(MqttEntity, VacuumEntity):
self._fan_speed = "unknown"
self._fan_speed_list = []
- MqttEntity.__init__(self, None, config, config_entry, discovery_data)
+ MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@staticmethod
def config_schema():
diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py
index 37a12d33df6..2c222af28d8 100644
--- a/homeassistant/components/mqtt/vacuum/schema_state.py
+++ b/homeassistant/components/mqtt/vacuum/schema_state.py
@@ -30,6 +30,7 @@ from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subs
from ... import mqtt
from ..debug_info import log_messages
from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity
+from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED
from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services
SERVICE_TO_STRING = {
@@ -135,22 +136,24 @@ PLATFORM_SCHEMA_STATE = (
async def async_setup_entity_state(
- config, async_add_entities, config_entry, discovery_data
+ hass, config, async_add_entities, config_entry, discovery_data
):
"""Set up a State MQTT Vacuum."""
- async_add_entities([MqttStateVacuum(config, config_entry, discovery_data)])
+ async_add_entities([MqttStateVacuum(hass, config, config_entry, discovery_data)])
class MqttStateVacuum(MqttEntity, StateVacuumEntity):
"""Representation of a MQTT-controlled state vacuum."""
- def __init__(self, config, config_entry, discovery_data):
+ _attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED
+
+ def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the vacuum."""
self._state = None
self._state_attrs = {}
self._fan_speed_list = []
- MqttEntity.__init__(self, None, config, config_entry, discovery_data)
+ MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@staticmethod
def config_schema():
diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py
index d89c947a4f3..44d10a66d5d 100644
--- a/homeassistant/components/mullvad/__init__.py
+++ b/homeassistant/components/mullvad/__init__.py
@@ -14,7 +14,7 @@ from .const import DOMAIN
PLATFORMS = ["binary_sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: dict):
+async def async_setup_entry(hass: HomeAssistant, entry: dict) -> bool:
"""Set up Mullvad VPN integration."""
async def async_get_mullvad_api_data():
diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py
index 7bee5ff5a9b..af14725a3b4 100644
--- a/homeassistant/components/mutesync/__init__.py
+++ b/homeassistant/components/mutesync/__init__.py
@@ -1,7 +1,6 @@
"""The mütesync integration."""
from __future__ import annotations
-from datetime import timedelta
import logging
import async_timeout
@@ -11,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import update_coordinator
-from .const import DOMAIN
+from .const import DOMAIN, UPDATE_INTERVAL_IN_MEETING, UPDATE_INTERVAL_NOT_IN_MEETING
PLATFORMS = ["binary_sensor"]
@@ -27,7 +26,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def update_data():
"""Update the data."""
async with async_timeout.timeout(2.5):
- return await client.get_state()
+ state = await client.get_state()
+
+ if state["muted"] is None or state["in_meeting"] is None:
+ raise update_coordinator.UpdateFailed("Got invalid response")
+
+ if state["in_meeting"]:
+ coordinator.update_interval = UPDATE_INTERVAL_IN_MEETING
+ else:
+ coordinator.update_interval = UPDATE_INTERVAL_NOT_IN_MEETING
+
+ return state
coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id
@@ -35,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass,
logging.getLogger(__name__),
name=DOMAIN,
- update_interval=timedelta(seconds=5),
+ update_interval=UPDATE_INTERVAL_NOT_IN_MEETING,
update_method=update_data,
)
await coordinator.async_config_entry_first_refresh()
diff --git a/homeassistant/components/mutesync/config_flow.py b/homeassistant/components/mutesync/config_flow.py
index e4964d552b0..99412ed1795 100644
--- a/homeassistant/components/mutesync/config_flow.py
+++ b/homeassistant/components/mutesync/config_flow.py
@@ -16,7 +16,7 @@ from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN
-STEP_USER_DATA_SCHEMA = vol.Schema({"host": str})
+STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required("host"): str})
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
diff --git a/homeassistant/components/mutesync/const.py b/homeassistant/components/mutesync/const.py
index fcf05584f42..5e288b405af 100644
--- a/homeassistant/components/mutesync/const.py
+++ b/homeassistant/components/mutesync/const.py
@@ -1,3 +1,8 @@
"""Constants for the mütesync integration."""
+from datetime import timedelta
+from typing import Final
-DOMAIN = "mutesync"
+DOMAIN: Final = "mutesync"
+
+UPDATE_INTERVAL_NOT_IN_MEETING: Final = timedelta(seconds=10)
+UPDATE_INTERVAL_IN_MEETING: Final = timedelta(seconds=5)
diff --git a/homeassistant/components/mutesync/translations/he.json b/homeassistant/components/mutesync/translations/he.json
new file mode 100644
index 00000000000..00011f86933
--- /dev/null
+++ b/homeassistant/components/mutesync/translations/he.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py
index a299968712a..063f044117e 100644
--- a/homeassistant/components/myq/__init__.py
+++ b/homeassistant/components/myq/__init__.py
@@ -17,7 +17,7 @@ from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTER
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up MyQ from a config entry."""
hass.data.setdefault(DOMAIN, {})
diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py
index b1b3680343d..96ab589253b 100644
--- a/homeassistant/components/myq/binary_sensor.py
+++ b/homeassistant/components/myq/binary_sensor.py
@@ -32,16 +32,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class MyQBinarySensorEntity(CoordinatorEntity, BinarySensorEntity):
"""Representation of a MyQ gateway."""
+ _attr_device_class = DEVICE_CLASS_CONNECTIVITY
+
def __init__(self, coordinator, device):
"""Initialize with API object, device id."""
super().__init__(coordinator)
self._device = device
- @property
- def device_class(self):
- """We track connectivity for gateways."""
- return DEVICE_CLASS_CONNECTIVITY
-
@property
def name(self):
"""Return the name of the garage door if any."""
diff --git a/homeassistant/components/myq/translations/he.json b/homeassistant/components/myq/translations/he.json
index ac90b3264ea..76815ee91d3 100644
--- a/homeassistant/components/myq/translations/he.json
+++ b/homeassistant/components/myq/translations/he.json
@@ -1,6 +1,21 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
+ "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",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
+ "reauth_confirm": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 {username} \u05d0\u05d9\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea \u05e2\u05d5\u05d3."
+ },
"user": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py
index 9d23cfd24b6..2a958cee060 100644
--- a/homeassistant/components/mysensors/__init__.py
+++ b/homeassistant/components/mysensors/__init__.py
@@ -40,9 +40,10 @@ from .const import (
MYSENSORS_ON_UNLOAD,
PLATFORMS_WITH_ENTRY_SUPPORT,
DevId,
+ DiscoveryInfo,
SensorType,
)
-from .device import MySensorsDevice, MySensorsEntity, get_mysensors_devices
+from .device import MySensorsDevice, get_mysensors_devices
from .gateway import finish_setup, get_mysensors_gateway, gw_stop, setup_gateway
from .helpers import on_unload
@@ -70,7 +71,7 @@ def set_default_persistence_file(value: dict) -> dict:
return value
-def has_all_unique_files(value):
+def has_all_unique_files(value: list[dict]) -> list[dict]:
"""Validate that all persistence files are unique and set if any is set."""
persistence_files = [gateway[CONF_PERSISTENCE_FILE] for gateway in value]
schema = vol.Schema(vol.Unique())
@@ -78,17 +79,17 @@ def has_all_unique_files(value):
return value
-def is_persistence_file(value):
+def is_persistence_file(value: str) -> str:
"""Validate that persistence file path ends in either .pickle or .json."""
if value.endswith((".json", ".pickle")):
return value
raise vol.Invalid(f"{value} does not end in either `.json` or `.pickle`")
-def deprecated(key):
+def deprecated(key: str) -> Callable[[dict], dict]:
"""Mark key as deprecated in configuration."""
- def validator(config):
+ def validator(config: dict) -> dict:
"""Check if key is in config, log warning and remove key."""
if key not in config:
return config
@@ -218,7 +219,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass_config=hass.data[DOMAIN][DATA_HASS_CONFIG],
)
- await on_unload(
+ on_unload(
hass,
entry.entry_id,
async_dispatcher_connect(
@@ -270,8 +271,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
def setup_mysensors_platform(
hass: HomeAssistant,
domain: str, # hass platform name
- discovery_info: dict[str, list[DevId]],
- device_class: type[MySensorsDevice] | dict[SensorType, type[MySensorsEntity]],
+ discovery_info: DiscoveryInfo,
+ device_class: type[MySensorsDevice] | dict[SensorType, type[MySensorsDevice]],
device_args: (
None | tuple
) = None, # extra arguments that will be given to the entity constructor
@@ -302,11 +303,13 @@ def setup_mysensors_platform(
if not gateway:
_LOGGER.warning("Skipping setup of %s, no gateway found", dev_id)
continue
- device_class_copy = device_class
+
if isinstance(device_class, dict):
child = gateway.sensors[node_id].children[child_id]
s_type = gateway.const.Presentation(child.type).name
device_class_copy = device_class[s_type]
+ else:
+ device_class_copy = device_class
args_copy = (*device_args, gateway_id, gateway, node_id, child_id, value_type)
devices[dev_id] = device_class_copy(*args_copy)
diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py
index 2077f38c758..f94b5f71728 100644
--- a/homeassistant/components/mysensors/binary_sensor.py
+++ b/homeassistant/components/mysensors/binary_sensor.py
@@ -1,4 +1,6 @@
"""Support for MySensors binary sensors."""
+from __future__ import annotations
+
from homeassistant.components import mysensors
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_MOISTURE,
@@ -10,7 +12,6 @@ from homeassistant.components.binary_sensor import (
DOMAIN,
BinarySensorEntity,
)
-from homeassistant.components.mysensors import on_unload
from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_ON
@@ -18,6 +19,9 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from .const import DiscoveryInfo
+from .helpers import on_unload
+
SENSORS = {
"S_DOOR": "door",
"S_MOTION": DEVICE_CLASS_MOTION,
@@ -34,11 +38,11 @@ async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
-):
+) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway)."""
@callback
- def async_discover(discovery_info):
+ def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors binary_sensor."""
mysensors.setup_mysensors_platform(
hass,
@@ -48,9 +52,9 @@ async def async_setup_entry(
async_add_entities=async_add_entities,
)
- await on_unload(
+ on_unload(
hass,
- config_entry,
+ config_entry.entry_id,
async_dispatcher_connect(
hass,
MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
@@ -63,12 +67,12 @@ class MySensorsBinarySensor(mysensors.device.MySensorsEntity, BinarySensorEntity
"""Representation of a MySensors Binary Sensor child node."""
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return True if the binary sensor is on."""
return self._values.get(self.value_type) == STATE_ON
@property
- def device_class(self):
+ def device_class(self) -> str | None:
"""Return the class of this sensor, from DEVICE_CLASSES."""
pres = self.gateway.const.Presentation
device_class = SENSORS.get(pres(self.child_type).name)
diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py
index f958f2274e0..5dd52673581 100644
--- a/homeassistant/components/mysensors/climate.py
+++ b/homeassistant/components/mysensors/climate.py
@@ -1,4 +1,8 @@
"""MySensors platform that offers a Climate (MySensors-HVAC) component."""
+from __future__ import annotations
+
+from typing import Any
+
from homeassistant.components import mysensors
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
@@ -13,14 +17,15 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
-from homeassistant.components.mysensors import on_unload
-from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
+from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY, DiscoveryInfo
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from .helpers import on_unload
+
DICT_HA_TO_MYS = {
HVAC_MODE_AUTO: "AutoChangeOver",
HVAC_MODE_COOL: "CoolOn",
@@ -42,10 +47,10 @@ async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
-):
+) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway)."""
- async def async_discover(discovery_info):
+ async def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors climate."""
mysensors.setup_mysensors_platform(
hass,
@@ -55,9 +60,9 @@ async def async_setup_entry(
async_add_entities=async_add_entities,
)
- await on_unload(
+ on_unload(
hass,
- config_entry,
+ config_entry.entry_id,
async_dispatcher_connect(
hass,
MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
@@ -70,7 +75,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
"""Representation of a MySensors HVAC."""
@property
- def supported_features(self):
+ def supported_features(self) -> int:
"""Return the list of supported features."""
features = 0
set_req = self.gateway.const.SetReq
@@ -86,22 +91,23 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
return features
@property
- def temperature_unit(self):
+ def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT
@property
- def current_temperature(self):
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
- value = self._values.get(self.gateway.const.SetReq.V_TEMP)
+ value: str | None = self._values.get(self.gateway.const.SetReq.V_TEMP)
+ float_value: float | None = None
if value is not None:
- value = float(value)
+ float_value = float(value)
- return value
+ return float_value
@property
- def target_temperature(self):
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
set_req = self.gateway.const.SetReq
if (
@@ -115,42 +121,46 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
return float(temp) if temp is not None else None
@property
- def target_temperature_high(self):
+ def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SETPOINT_HEAT in self._values:
temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
return float(temp) if temp is not None else None
+ return None
+
@property
- def target_temperature_low(self):
+ def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SETPOINT_COOL in self._values:
temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
return float(temp) if temp is not None else None
- @property
- def hvac_mode(self):
- """Return current operation ie. heat, cool, idle."""
- return self._values.get(self.value_type)
+ return None
@property
- def hvac_modes(self):
+ def hvac_mode(self) -> str:
+ """Return current operation ie. heat, cool, idle."""
+ return self._values.get(self.value_type, HVAC_MODE_HEAT)
+
+ @property
+ def hvac_modes(self) -> list[str]:
"""List of available operation modes."""
return OPERATION_LIST
@property
- def fan_mode(self):
+ def fan_mode(self) -> str | None:
"""Return the fan setting."""
return self._values.get(self.gateway.const.SetReq.V_HVAC_SPEED)
@property
- def fan_modes(self):
+ def fan_modes(self) -> list[str]:
"""List of available fan modes."""
return FAN_LIST
- async def async_set_temperature(self, **kwargs):
+ async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
set_req = self.gateway.const.SetReq
temp = kwargs.get(ATTR_TEMPERATURE)
@@ -182,7 +192,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
self._values[value_type] = value
self.async_write_ha_state()
- async def async_set_fan_mode(self, fan_mode):
+ async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target temperature."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
@@ -193,7 +203,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
self._values[set_req.V_HVAC_SPEED] = fan_mode
self.async_write_ha_state()
- async def async_set_hvac_mode(self, hvac_mode):
+ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target temperature."""
self.gateway.set_child_value(
self.node_id,
@@ -207,7 +217,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
self._values[self.value_type] = hvac_mode
self.async_write_ha_state()
- async def async_update(self):
+ async def async_update(self) -> None:
"""Update the controller with the latest value from a sensor."""
await super().async_update()
self._values[self.value_type] = DICT_MYS_TO_HA[self._values[self.value_type]]
diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py
index 6676e11febf..920cb40b7ab 100644
--- a/homeassistant/components/mysensors/config_flow.py
+++ b/homeassistant/components/mysensors/config_flow.py
@@ -1,7 +1,6 @@
"""Config flow for MySensors."""
from __future__ import annotations
-from contextlib import suppress
import logging
import os
from typing import Any
@@ -55,7 +54,6 @@ def _get_schema_common(user_input: dict[str, str]) -> dict:
schema = {
vol.Required(
CONF_VERSION,
- default="",
description={
"suggested_value": user_input.get(CONF_VERSION, DEFAULT_VERSION)
},
@@ -67,14 +65,14 @@ def _get_schema_common(user_input: dict[str, str]) -> dict:
def _validate_version(version: str) -> dict[str, str]:
"""Validate a version string from the user."""
- version_okay = False
- with suppress(AwesomeVersionStrategyException):
- version_okay = bool(
- AwesomeVersion.ensure_strategy(
- version,
- [AwesomeVersionStrategy.SIMPLEVER, AwesomeVersionStrategy.SEMVER],
- )
+ version_okay = True
+ try:
+ AwesomeVersion(
+ version,
+ [AwesomeVersionStrategy.SIMPLEVER, AwesomeVersionStrategy.SEMVER],
)
+ except AwesomeVersionStrategyException:
+ version_okay = False
if version_okay:
return {}
@@ -82,8 +80,8 @@ def _validate_version(version: str) -> dict[str, str]:
def _is_same_device(
- gw_type: ConfGatewayType, user_input: dict[str, str], entry: ConfigEntry
-):
+ gw_type: ConfGatewayType, user_input: dict[str, Any], entry: ConfigEntry
+) -> bool:
"""Check if another ConfigDevice is actually the same as user_input.
This function only compares addresses and tcp ports, so it is possible to fool it with tricks like port forwarding.
@@ -91,7 +89,9 @@ def _is_same_device(
if entry.data[CONF_DEVICE] != user_input[CONF_DEVICE]:
return False
if gw_type == CONF_GATEWAY_TYPE_TCP:
- return entry.data[CONF_TCP_PORT] == user_input[CONF_TCP_PORT]
+ entry_tcp_port: int = entry.data[CONF_TCP_PORT]
+ input_tcp_port: int = user_input[CONF_TCP_PORT]
+ return entry_tcp_port == input_tcp_port
if gw_type == CONF_GATEWAY_TYPE_MQTT:
entry_topics = {
entry.data[CONF_TOPIC_IN_PREFIX],
@@ -111,7 +111,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Set up config flow."""
self._gw_type: str | None = None
- async def async_step_import(self, user_input: dict[str, str] | None = None):
+ async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult:
"""Import a config entry.
This method is called by async_setup and it has already
@@ -131,12 +131,14 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
else:
user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_SERIAL
- result: dict[str, Any] = await self.async_step_user(user_input=user_input)
- if result["type"] == "form":
- return self.async_abort(reason=next(iter(result["errors"].values())))
+ result: FlowResult = await self.async_step_user(user_input=user_input)
+ if errors := result.get("errors"):
+ return self.async_abort(reason=next(iter(errors.values())))
return result
- async def async_step_user(self, user_input: dict[str, str] | None = None):
+ async def async_step_user(
+ self, user_input: dict[str, str] | None = None
+ ) -> FlowResult:
"""Create a config entry from frontend user input."""
schema = {vol.Required(CONF_GATEWAY_TYPE): vol.In(CONF_GATEWAY_TYPE_ALL)}
schema = vol.Schema(schema)
@@ -158,9 +160,11 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
- async def async_step_gw_serial(self, user_input: dict[str, str] | None = None):
+ async def async_step_gw_serial(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
"""Create config entry for a serial gateway."""
- errors = {}
+ errors: dict[str, str] = {}
if user_input is not None:
errors.update(
await self.validate_common(CONF_GATEWAY_TYPE_SERIAL, errors, user_input)
@@ -187,7 +191,9 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
step_id="gw_serial", data_schema=schema, errors=errors
)
- async def async_step_gw_tcp(self, user_input: dict[str, str] | None = None):
+ async def async_step_gw_tcp(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
"""Create a config entry for a tcp gateway."""
errors = {}
if user_input is not None:
@@ -225,7 +231,9 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return True
return False
- async def async_step_gw_mqtt(self, user_input: dict[str, str] | None = None):
+ async def async_step_gw_mqtt(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
"""Create a config entry for a mqtt gateway."""
errors = {}
if user_input is not None:
@@ -280,9 +288,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
)
@callback
- def _async_create_entry(
- self, user_input: dict[str, str] | None = None
- ) -> FlowResult:
+ def _async_create_entry(self, user_input: dict[str, Any]) -> FlowResult:
"""Create the config entry."""
return self.async_create_entry(
title=f"{user_input[CONF_DEVICE]}",
@@ -296,55 +302,52 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self,
gw_type: ConfGatewayType,
errors: dict[str, str],
- user_input: dict[str, str] | None = None,
+ user_input: dict[str, Any],
) -> dict[str, str]:
"""Validate parameters common to all gateway types."""
- if user_input is not None:
- errors.update(_validate_version(user_input.get(CONF_VERSION)))
+ errors.update(_validate_version(user_input[CONF_VERSION]))
- if gw_type != CONF_GATEWAY_TYPE_MQTT:
- if gw_type == CONF_GATEWAY_TYPE_TCP:
- verification_func = is_socket_address
- else:
- verification_func = is_serial_port
+ if gw_type != CONF_GATEWAY_TYPE_MQTT:
+ if gw_type == CONF_GATEWAY_TYPE_TCP:
+ verification_func = is_socket_address
+ else:
+ verification_func = is_serial_port
- try:
- await self.hass.async_add_executor_job(
- verification_func, user_input.get(CONF_DEVICE)
- )
- except vol.Invalid:
- errors[CONF_DEVICE] = (
- "invalid_ip"
- if gw_type == CONF_GATEWAY_TYPE_TCP
- else "invalid_serial"
- )
- if CONF_PERSISTENCE_FILE in user_input:
- try:
- is_persistence_file(user_input[CONF_PERSISTENCE_FILE])
- except vol.Invalid:
- errors[CONF_PERSISTENCE_FILE] = "invalid_persistence_file"
- else:
- real_persistence_path = user_input[
- CONF_PERSISTENCE_FILE
- ] = self._normalize_persistence_file(
- user_input[CONF_PERSISTENCE_FILE]
- )
- for other_entry in self._async_current_entries():
- if CONF_PERSISTENCE_FILE not in other_entry.data:
- continue
- if real_persistence_path == self._normalize_persistence_file(
- other_entry.data[CONF_PERSISTENCE_FILE]
- ):
- errors[CONF_PERSISTENCE_FILE] = "duplicate_persistence_file"
- break
+ try:
+ await self.hass.async_add_executor_job(
+ verification_func, user_input.get(CONF_DEVICE)
+ )
+ except vol.Invalid:
+ errors[CONF_DEVICE] = (
+ "invalid_ip"
+ if gw_type == CONF_GATEWAY_TYPE_TCP
+ else "invalid_serial"
+ )
+ if CONF_PERSISTENCE_FILE in user_input:
+ try:
+ is_persistence_file(user_input[CONF_PERSISTENCE_FILE])
+ except vol.Invalid:
+ errors[CONF_PERSISTENCE_FILE] = "invalid_persistence_file"
+ else:
+ real_persistence_path = user_input[
+ CONF_PERSISTENCE_FILE
+ ] = self._normalize_persistence_file(user_input[CONF_PERSISTENCE_FILE])
+ for other_entry in self._async_current_entries():
+ if CONF_PERSISTENCE_FILE not in other_entry.data:
+ continue
+ if real_persistence_path == self._normalize_persistence_file(
+ other_entry.data[CONF_PERSISTENCE_FILE]
+ ):
+ errors[CONF_PERSISTENCE_FILE] = "duplicate_persistence_file"
+ break
- for other_entry in self._async_current_entries():
- if _is_same_device(gw_type, user_input, other_entry):
- errors["base"] = "already_configured"
- break
+ for other_entry in self._async_current_entries():
+ if _is_same_device(gw_type, user_input, other_entry):
+ errors["base"] = "already_configured"
+ break
- # if no errors so far, try to connect
- if not errors and not await try_connect(self.hass, user_input):
- errors["base"] = "cannot_connect"
+ # if no errors so far, try to connect
+ if not errors and not await try_connect(self.hass, gw_type, user_input):
+ errors["base"] = "cannot_connect"
return errors
diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py
index 1bd071be9a9..f8e157e3622 100644
--- a/homeassistant/components/mysensors/const.py
+++ b/homeassistant/components/mysensors/const.py
@@ -2,23 +2,23 @@
from __future__ import annotations
from collections import defaultdict
-from typing import Literal, Tuple
+from typing import Final, Literal, Tuple, TypedDict
-ATTR_DEVICES: str = "devices"
-ATTR_GATEWAY_ID: str = "gateway_id"
+ATTR_DEVICES: Final = "devices"
+ATTR_GATEWAY_ID: Final = "gateway_id"
-CONF_BAUD_RATE: str = "baud_rate"
-CONF_DEVICE: str = "device"
-CONF_GATEWAYS: str = "gateways"
-CONF_NODES: str = "nodes"
-CONF_PERSISTENCE: str = "persistence"
-CONF_PERSISTENCE_FILE: str = "persistence_file"
-CONF_RETAIN: str = "retain"
-CONF_TCP_PORT: str = "tcp_port"
-CONF_TOPIC_IN_PREFIX: str = "topic_in_prefix"
-CONF_TOPIC_OUT_PREFIX: str = "topic_out_prefix"
-CONF_VERSION: str = "version"
-CONF_GATEWAY_TYPE: str = "gateway_type"
+CONF_BAUD_RATE: Final = "baud_rate"
+CONF_DEVICE: Final = "device"
+CONF_GATEWAYS: Final = "gateways"
+CONF_NODES: Final = "nodes"
+CONF_PERSISTENCE: Final = "persistence"
+CONF_PERSISTENCE_FILE: Final = "persistence_file"
+CONF_RETAIN: Final = "retain"
+CONF_TCP_PORT: Final = "tcp_port"
+CONF_TOPIC_IN_PREFIX: Final = "topic_in_prefix"
+CONF_TOPIC_OUT_PREFIX: Final = "topic_out_prefix"
+CONF_VERSION: Final = "version"
+CONF_GATEWAY_TYPE: Final = "gateway_type"
ConfGatewayType = Literal["Serial", "TCP", "MQTT"]
CONF_GATEWAY_TYPE_SERIAL: ConfGatewayType = "Serial"
CONF_GATEWAY_TYPE_TCP: ConfGatewayType = "TCP"
@@ -29,19 +29,28 @@ CONF_GATEWAY_TYPE_ALL: list[str] = [
CONF_GATEWAY_TYPE_TCP,
]
-DOMAIN: str = "mysensors"
+DOMAIN: Final = "mysensors"
MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}"
-MYSENSORS_GATEWAYS: str = "mysensors_gateways"
-PLATFORM: str = "platform"
-SCHEMA: str = "schema"
+MYSENSORS_GATEWAYS: Final = "mysensors_gateways"
+PLATFORM: Final = "platform"
+SCHEMA: Final = "schema"
CHILD_CALLBACK: str = "mysensors_child_callback_{}_{}_{}_{}"
NODE_CALLBACK: str = "mysensors_node_callback_{}_{}"
-MYSENSORS_DISCOVERY = "mysensors_discovery_{}_{}"
-MYSENSORS_ON_UNLOAD = "mysensors_on_unload_{}"
-TYPE: str = "type"
+MYSENSORS_DISCOVERY: str = "mysensors_discovery_{}_{}"
+MYSENSORS_ON_UNLOAD: str = "mysensors_on_unload_{}"
+TYPE: Final = "type"
UPDATE_DELAY: float = 0.1
-SERVICE_SEND_IR_CODE: str = "send_ir_code"
+
+class DiscoveryInfo(TypedDict):
+ """Represent the discovery info type for mysensors platforms."""
+
+ devices: list[DevId]
+ name: str # CONF_NAME is used in the notify base integration.
+ gateway_id: GatewayId
+
+
+SERVICE_SEND_IR_CODE: Final = "send_ir_code"
SensorType = str
# S_DOOR, S_MOTION, S_SMOKE, ...
diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py
index 031efc97209..bab7a07a867 100644
--- a/homeassistant/components/mysensors/cover.py
+++ b/homeassistant/components/mysensors/cover.py
@@ -1,17 +1,21 @@
"""Support for MySensors covers."""
+from __future__ import annotations
+
from enum import Enum, unique
import logging
+from typing import Any
from homeassistant.components import mysensors
from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity
-from homeassistant.components.mysensors import on_unload
-from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
+from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY, DiscoveryInfo
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from .helpers import on_unload
+
_LOGGER = logging.getLogger(__name__)
@@ -29,10 +33,10 @@ async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
-):
+) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway)."""
- async def async_discover(discovery_info):
+ async def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors cover."""
mysensors.setup_mysensors_platform(
hass,
@@ -42,7 +46,7 @@ async def async_setup_entry(
async_add_entities=async_add_entities,
)
- await on_unload(
+ on_unload(
hass,
config_entry.entry_id,
async_dispatcher_connect(
@@ -56,7 +60,7 @@ async def async_setup_entry(
class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
"""Representation of the value of a MySensors Cover child node."""
- def get_cover_state(self):
+ def get_cover_state(self) -> CoverState:
"""Return a CoverState enum representing the state of the cover."""
set_req = self.gateway.const.SetReq
v_up = self._values.get(set_req.V_UP) == STATE_ON
@@ -68,7 +72,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
# or V_STATUS.
amount = 100
if set_req.V_DIMMER in self._values:
- amount = self._values.get(set_req.V_DIMMER)
+ amount = self._values[set_req.V_DIMMER]
else:
amount = 100 if self._values.get(set_req.V_LIGHT) == STATE_ON else 0
@@ -81,22 +85,22 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
return CoverState.OPEN
@property
- def is_closed(self):
+ def is_closed(self) -> bool:
"""Return True if the cover is closed."""
return self.get_cover_state() == CoverState.CLOSED
@property
- def is_closing(self):
+ def is_closing(self) -> bool:
"""Return True if the cover is closing."""
return self.get_cover_state() == CoverState.CLOSING
@property
- def is_opening(self):
+ def is_opening(self) -> bool:
"""Return True if the cover is opening."""
return self.get_cover_state() == CoverState.OPENING
@property
- def current_cover_position(self):
+ def current_cover_position(self) -> int | None:
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
@@ -104,7 +108,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
set_req = self.gateway.const.SetReq
return self._values.get(set_req.V_DIMMER)
- async def async_open_cover(self, **kwargs):
+ async def async_open_cover(self, **kwargs: Any) -> None:
"""Move the cover up."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
@@ -118,7 +122,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
self._values[set_req.V_LIGHT] = STATE_ON
self.async_write_ha_state()
- async def async_close_cover(self, **kwargs):
+ async def async_close_cover(self, **kwargs: Any) -> None:
"""Move the cover down."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
@@ -132,7 +136,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
self._values[set_req.V_LIGHT] = STATE_OFF
self.async_write_ha_state()
- async def async_set_cover_position(self, **kwargs):
+ async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
position = kwargs.get(ATTR_POSITION)
set_req = self.gateway.const.SetReq
@@ -144,7 +148,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
self._values[set_req.V_DIMMER] = position
self.async_write_ha_state()
- async def async_stop_cover(self, **kwargs):
+ async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the device."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py
index c1d8c431bc0..32305061ca7 100644
--- a/homeassistant/components/mysensors/device.py
+++ b/homeassistant/components/mysensors/device.py
@@ -3,12 +3,13 @@ from __future__ import annotations
from functools import partial
import logging
+from typing import Any
from mysensors import BaseAsyncGateway, Sensor
from mysensors.sensor import ChildSensor
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity
@@ -36,6 +37,8 @@ MYSENSORS_PLATFORM_DEVICES = "mysensors_devices_{}"
class MySensorsDevice:
"""Representation of a MySensors device."""
+ hass: HomeAssistant
+
def __init__(
self,
gateway_id: GatewayId,
@@ -51,9 +54,8 @@ class MySensorsDevice:
self.child_id: int = child_id
self.value_type: int = value_type # value_type as int. string variant can be looked up in gateway consts
self.child_type = self._child.type
- self._values = {}
+ self._values: dict[int, Any] = {}
self._update_scheduled = False
- self.hass = None
@property
def dev_id(self) -> DevId:
@@ -64,10 +66,10 @@ class MySensorsDevice:
return self.gateway_id, self.node_id, self.child_id, self.value_type
@property
- def _logger(self):
+ def _logger(self) -> logging.Logger:
return logging.getLogger(f"{__name__}.{self.name}")
- async def async_will_remove_from_hass(self):
+ async def async_will_remove_from_hass(self) -> None:
"""Remove this entity from home assistant."""
for platform in PLATFORM_TYPES:
platform_str = MYSENSORS_PLATFORM_DEVICES.format(platform)
@@ -89,17 +91,26 @@ class MySensorsDevice:
@property
def sketch_name(self) -> str:
- """Return the name of the sketch running on the whole node (will be the same for several entities!)."""
- return self._node.sketch_name
+ """Return the name of the sketch running on the whole node.
+
+ The name will be the same for several entities.
+ """
+ return self._node.sketch_name # type: ignore[no-any-return]
@property
def sketch_version(self) -> str:
- """Return the version of the sketch running on the whole node (will be the same for several entities!)."""
- return self._node.sketch_version
+ """Return the version of the sketch running on the whole node.
+
+ The name will be the same for several entities.
+ """
+ return self._node.sketch_version # type: ignore[no-any-return]
@property
def node_name(self) -> str:
- """Name of the whole node (will be the same for several entities!)."""
+ """Name of the whole node.
+
+ The name will be the same for several entities.
+ """
return f"{self.sketch_name} {self.node_id}"
@property
@@ -109,7 +120,7 @@ class MySensorsDevice:
@property
def device_info(self) -> DeviceInfo:
- """Return a dict that allows home assistant to puzzle all entities belonging to a node together."""
+ """Return the device info."""
return {
"identifiers": {(DOMAIN, f"{self.gateway_id}-{self.node_id}")},
"name": self.node_name,
@@ -118,13 +129,13 @@ class MySensorsDevice:
}
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of this entity."""
return f"{self.node_name} {self.child_id}"
@property
- def extra_state_attributes(self):
- """Return device specific state attributes."""
+ def _extra_attributes(self) -> dict[str, Any]:
+ """Return device specific attributes."""
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
attr = {
@@ -134,10 +145,6 @@ class MySensorsDevice:
ATTR_DESCRIPTION: child.description,
ATTR_NODE_ID: self.node_id,
}
- # This works when we are actually an Entity (i.e. all platforms except device_tracker)
- if hasattr(self, "platform"):
- # pylint: disable=no-member
- attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE]
set_req = self.gateway.const.SetReq
@@ -146,7 +153,7 @@ class MySensorsDevice:
return attr
- async def async_update(self):
+ async def async_update(self) -> None:
"""Update the controller with the latest value from a sensor."""
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
@@ -173,17 +180,17 @@ class MySensorsDevice:
else:
self._values[value_type] = value
- async def _async_update_callback(self):
+ async def _async_update_callback(self) -> None:
"""Update the device."""
raise NotImplementedError
@callback
- def async_update_callback(self):
+ def async_update_callback(self) -> None:
"""Update the device after delay."""
if self._update_scheduled:
return
- async def update():
+ async def update() -> None:
"""Perform update."""
try:
await self._async_update_callback()
@@ -197,31 +204,47 @@ class MySensorsDevice:
self.hass.loop.call_later(UPDATE_DELAY, delayed_update)
-def get_mysensors_devices(hass, domain: str) -> dict[DevId, MySensorsDevice]:
+def get_mysensors_devices(
+ hass: HomeAssistant, domain: str
+) -> dict[DevId, MySensorsDevice]:
"""Return MySensors devices for a hass platform name."""
if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data[DOMAIN]:
hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] = {}
- return hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)]
+ devices: dict[DevId, MySensorsDevice] = hass.data[DOMAIN][
+ MYSENSORS_PLATFORM_DEVICES.format(domain)
+ ]
+ return devices
class MySensorsEntity(MySensorsDevice, Entity):
"""Representation of a MySensors entity."""
@property
- def should_poll(self):
+ def should_poll(self) -> bool:
"""Return the polling state. The gateway pushes its states."""
return False
@property
- def available(self):
+ def available(self) -> bool:
"""Return true if entity is available."""
return self.value_type in self._values
- async def _async_update_callback(self):
+ @property
+ def extra_state_attributes(self) -> dict[str, Any]:
+ """Return entity specific state attributes."""
+ attr = self._extra_attributes
+
+ assert self.platform
+ assert self.platform.config_entry
+ attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE]
+
+ return attr
+
+ async def _async_update_callback(self) -> None:
"""Update the entity."""
await self.async_update_ha_state(True)
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Register update callback."""
self.async_on_remove(
async_dispatcher_connect(
diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py
index 45416ff7ae7..544fb8d6b09 100644
--- a/homeassistant/components/mysensors/device_tracker.py
+++ b/homeassistant/components/mysensors/device_tracker.py
@@ -1,16 +1,29 @@
"""Support for tracking MySensors devices."""
+from __future__ import annotations
+
+from typing import Any, Callable
+
from homeassistant.components import mysensors
from homeassistant.components.device_tracker import DOMAIN
-from homeassistant.components.mysensors import DevId, on_unload
-from homeassistant.components.mysensors.const import ATTR_GATEWAY_ID, GatewayId
+from homeassistant.components.mysensors import DevId
+from homeassistant.components.mysensors.const import (
+ ATTR_GATEWAY_ID,
+ DiscoveryInfo,
+ GatewayId,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify
+from .helpers import on_unload
+
async def async_setup_scanner(
- hass: HomeAssistant, config, async_see, discovery_info=None
-):
+ hass: HomeAssistant,
+ config: dict[str, Any],
+ async_see: Callable,
+ discovery_info: DiscoveryInfo | None = None,
+) -> bool:
"""Set up the MySensors device scanner."""
if not discovery_info:
return False
@@ -28,7 +41,7 @@ async def async_setup_scanner(
for device in new_devices:
gateway_id: GatewayId = discovery_info[ATTR_GATEWAY_ID]
dev_id: DevId = (gateway_id, device.node_id, device.child_id, device.value_type)
- await on_unload(
+ on_unload(
hass,
gateway_id,
async_dispatcher_connect(
@@ -37,7 +50,7 @@ async def async_setup_scanner(
device.async_update_callback,
),
)
- await on_unload(
+ on_unload(
hass,
gateway_id,
async_dispatcher_connect(
@@ -53,13 +66,13 @@ async def async_setup_scanner(
class MySensorsDeviceScanner(mysensors.device.MySensorsDevice):
"""Represent a MySensors scanner."""
- def __init__(self, hass: HomeAssistant, async_see, *args):
+ def __init__(self, hass: HomeAssistant, async_see: Callable, *args: Any) -> None:
"""Set up instance."""
super().__init__(*args)
self.async_see = async_see
self.hass = hass
- async def _async_update_callback(self):
+ async def _async_update_callback(self) -> None:
"""Update the device."""
await self.async_update()
node = self.gateway.sensors[self.node_id]
@@ -72,5 +85,5 @@ class MySensorsDeviceScanner(mysensors.device.MySensorsDevice):
host_name=self.name,
gps=(latitude, longitude),
battery=node.battery_level,
- attributes=self.extra_state_attributes,
+ attributes=self._extra_attributes,
)
diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py
index ec403e6e34b..f1e2cd0a4e1 100644
--- a/homeassistant/components/mysensors/gateway.py
+++ b/homeassistant/components/mysensors/gateway.py
@@ -14,6 +14,10 @@ from mysensors import BaseAsyncGateway, Message, Sensor, mysensors
import voluptuous as vol
from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN
+from homeassistant.components.mqtt.models import (
+ Message as MQTTMessage,
+ PublishPayloadType,
+)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback
@@ -22,6 +26,9 @@ import homeassistant.helpers.config_validation as cv
from .const import (
CONF_BAUD_RATE,
CONF_DEVICE,
+ CONF_GATEWAY_TYPE,
+ CONF_GATEWAY_TYPE_MQTT,
+ CONF_GATEWAY_TYPE_SERIAL,
CONF_PERSISTENCE_FILE,
CONF_RETAIN,
CONF_TCP_PORT,
@@ -31,6 +38,7 @@ from .const import (
DOMAIN,
MYSENSORS_GATEWAY_START_TASK,
MYSENSORS_GATEWAYS,
+ ConfGatewayType,
GatewayId,
)
from .handler import HANDLERS
@@ -47,7 +55,7 @@ GATEWAY_READY_TIMEOUT = 20.0
MQTT_COMPONENT = "mqtt"
-def is_serial_port(value):
+def is_serial_port(value: str) -> str:
"""Validate that value is a windows serial port or a unix device."""
if sys.platform.startswith("win"):
ports = (f"COM{idx + 1}" for idx in range(256))
@@ -57,7 +65,7 @@ def is_serial_port(value):
return cv.isdevice(value)
-def is_socket_address(value):
+def is_socket_address(value: str) -> str:
"""Validate that value is a valid address."""
try:
socket.getaddrinfo(value, None)
@@ -66,10 +74,12 @@ def is_socket_address(value):
raise vol.Invalid("Device is not a valid domain name or ip address") from err
-async def try_connect(hass: HomeAssistant, user_input: dict[str, str]) -> bool:
+async def try_connect(
+ hass: HomeAssistant, gateway_type: ConfGatewayType, user_input: dict[str, Any]
+) -> bool:
"""Try to connect to a gateway and report if it worked."""
- if user_input[CONF_DEVICE] == MQTT_COMPONENT:
- return True # dont validate mqtt. mqtt gateways dont send ready messages :(
+ if gateway_type == "MQTT":
+ return True # Do not validate MQTT, as that does not use connection made.
try:
gateway_ready = asyncio.Event()
@@ -78,6 +88,7 @@ async def try_connect(hass: HomeAssistant, user_input: dict[str, str]) -> bool:
gateway: BaseAsyncGateway | None = await _get_gateway(
hass,
+ gateway_type,
device=user_input[CONF_DEVICE],
version=user_input[CONF_VERSION],
event_callback=lambda _: None,
@@ -128,6 +139,7 @@ async def setup_gateway(
ready_gateway = await _get_gateway(
hass,
+ gateway_type=entry.data[CONF_GATEWAY_TYPE],
device=entry.data[CONF_DEVICE],
version=entry.data[CONF_VERSION],
event_callback=_gw_callback_factory(hass, entry.entry_id),
@@ -145,6 +157,7 @@ async def setup_gateway(
async def _get_gateway(
hass: HomeAssistant,
+ gateway_type: ConfGatewayType,
device: str,
version: str,
event_callback: Callable[[Message], None],
@@ -154,30 +167,33 @@ async def _get_gateway(
topic_in_prefix: str | None = None,
topic_out_prefix: str | None = None,
retain: bool = False,
- persistence: bool = True, # old persistence option has been deprecated. kwarg is here so we can run try_connect() without persistence
+ persistence: bool = True,
) -> BaseAsyncGateway | None:
"""Return gateway after setup of the gateway."""
if persistence_file is not None:
- # interpret relative paths to be in hass config folder. absolute paths will be left as they are
+ # Interpret relative paths to be in hass config folder.
+ # Absolute paths will be left as they are.
persistence_file = hass.config.path(persistence_file)
- if device == MQTT_COMPONENT:
+ if gateway_type == CONF_GATEWAY_TYPE_MQTT:
# Make sure the mqtt integration is set up.
# Naive check that doesn't consider config entry state.
if MQTT_DOMAIN not in hass.config.components:
return None
mqtt = hass.components.mqtt
- def pub_callback(topic, payload, qos, retain):
+ def pub_callback(topic: str, payload: str, qos: int, retain: bool) -> None:
"""Call MQTT publish function."""
mqtt.async_publish(topic, payload, qos, retain)
- def sub_callback(topic, sub_cb, qos):
+ def sub_callback(
+ topic: str, sub_cb: Callable[[str, PublishPayloadType, int], None], qos: int
+ ) -> None:
"""Call MQTT subscribe function."""
@callback
- def internal_callback(msg):
+ def internal_callback(msg: MQTTMessage) -> None:
"""Call callback."""
sub_cb(msg.topic, msg.payload, msg.qos)
@@ -195,35 +211,26 @@ async def _get_gateway(
persistence_file=persistence_file,
protocol_version=version,
)
+ elif gateway_type == CONF_GATEWAY_TYPE_SERIAL:
+ gateway = mysensors.AsyncSerialGateway(
+ device,
+ baud=baud_rate,
+ loop=hass.loop,
+ event_callback=None,
+ persistence=persistence,
+ persistence_file=persistence_file,
+ protocol_version=version,
+ )
else:
- try:
- await hass.async_add_executor_job(is_serial_port, device)
- gateway = mysensors.AsyncSerialGateway(
- device,
- baud=baud_rate,
- loop=hass.loop,
- event_callback=None,
- persistence=persistence,
- persistence_file=persistence_file,
- protocol_version=version,
- )
- except vol.Invalid:
- try:
- await hass.async_add_executor_job(is_socket_address, device)
- # valid ip address
- gateway = mysensors.AsyncTCPGateway(
- device,
- port=tcp_port,
- loop=hass.loop,
- event_callback=None,
- persistence=persistence,
- persistence_file=persistence_file,
- protocol_version=version,
- )
- except vol.Invalid:
- # invalid ip address
- _LOGGER.error("Connect failed: Invalid device %s", device)
- return None
+ gateway = mysensors.AsyncTCPGateway(
+ device,
+ port=tcp_port,
+ loop=hass.loop,
+ event_callback=None,
+ persistence=persistence,
+ persistence_file=persistence_file,
+ protocol_version=version,
+ )
gateway.event_callback = event_callback
if persistence:
await gateway.start_persistence()
@@ -233,7 +240,7 @@ async def _get_gateway(
async def finish_setup(
hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway
-):
+) -> None:
"""Load any persistent devices and platforms and start gateway."""
discover_tasks = []
start_tasks = []
@@ -248,9 +255,8 @@ async def finish_setup(
async def _discover_persistent_devices(
hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway
-):
+) -> None:
"""Discover platforms for devices loaded via persistence file."""
- tasks = []
new_devices = defaultdict(list)
for node_id in gateway.sensors:
if not validate_node(gateway, node_id):
@@ -263,11 +269,11 @@ async def _discover_persistent_devices(
_LOGGER.debug("discovering persistent devices: %s", new_devices)
for platform, dev_ids in new_devices.items():
discover_mysensors_platform(hass, entry.entry_id, platform, dev_ids)
- if tasks:
- await asyncio.wait(tasks)
-async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway):
+async def gw_stop(
+ hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway
+) -> None:
"""Stop the gateway."""
connect_task = hass.data[DOMAIN].pop(
MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id), None
@@ -277,11 +283,14 @@ async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway):
await gateway.stop()
-async def _gw_start(hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway):
+async def _gw_start(
+ hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway
+) -> None:
"""Start the gateway."""
gateway_ready = asyncio.Event()
- def gateway_connected(_: BaseAsyncGateway):
+ def gateway_connected(_: BaseAsyncGateway) -> None:
+ """Handle gateway connected."""
gateway_ready.set()
gateway.on_conn_made = gateway_connected
@@ -292,10 +301,11 @@ async def _gw_start(hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncG
gateway.start()
) # store the connect task so it can be cancelled in gw_stop
- async def stop_this_gw(_: Event):
+ async def stop_this_gw(_: Event) -> None:
+ """Stop the gateway."""
await gw_stop(hass, entry, gateway)
- await on_unload(
+ on_unload(
hass,
entry.entry_id,
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_this_gw),
@@ -321,7 +331,7 @@ def _gw_callback_factory(
"""Return a new callback for the gateway."""
@callback
- def mysensors_callback(msg: Message):
+ def mysensors_callback(msg: Message) -> None:
"""Handle messages from a MySensors gateway.
All MySenors messages are received here.
@@ -331,8 +341,8 @@ def _gw_callback_factory(
msg_type = msg.gateway.const.MessageType(msg.type)
msg_handler: Callable[
- [Any, GatewayId, Message], Coroutine[None]
- ] = HANDLERS.get(msg_type.name)
+ [HomeAssistant, GatewayId, Message], Coroutine[Any, Any, None]
+ ] | None = HANDLERS.get(msg_type.name)
if msg_handler is None:
return
diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py
index 8558cd01f42..0fb86fd0eec 100644
--- a/homeassistant/components/mysensors/handler.py
+++ b/homeassistant/components/mysensors/handler.py
@@ -68,7 +68,7 @@ async def handle_sketch_version(
@callback
def _handle_child_update(
hass: HomeAssistant, gateway_id: GatewayId, validated: dict[str, list[DevId]]
-):
+) -> None:
"""Handle a child update."""
signals: list[str] = []
@@ -91,7 +91,9 @@ def _handle_child_update(
@callback
-def _handle_node_update(hass: HomeAssistant, gateway_id: GatewayId, msg: Message):
+def _handle_node_update(
+ hass: HomeAssistant, gateway_id: GatewayId, msg: Message
+) -> None:
"""Handle a node update."""
signal = NODE_CALLBACK.format(gateway_id, msg.node_id)
async_dispatcher_send(hass, signal)
diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py
index 9a35f67d49b..7c50526cd6e 100644
--- a/homeassistant/components/mysensors/helpers.py
+++ b/homeassistant/components/mysensors/helpers.py
@@ -10,7 +10,6 @@ from mysensors import BaseAsyncGateway, Message
from mysensors.sensor import ChildSensor
import voluptuous as vol
-from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
@@ -35,18 +34,13 @@ _LOGGER = logging.getLogger(__name__)
SCHEMAS = Registry()
-async def on_unload(
- hass: HomeAssistant, entry: ConfigEntry | GatewayId, fnct: Callable
-) -> None:
+@callback
+def on_unload(hass: HomeAssistant, gateway_id: GatewayId, fnct: Callable) -> None:
"""Register a callback to be called when entry is unloaded.
This function is used by platforms to cleanup after themselves.
"""
- if isinstance(entry, GatewayId):
- uniqueid = entry
- else:
- uniqueid = entry.entry_id
- key = MYSENSORS_ON_UNLOAD.format(uniqueid)
+ key = MYSENSORS_ON_UNLOAD.format(gateway_id)
if key not in hass.data[DOMAIN]:
hass.data[DOMAIN][key] = []
hass.data[DOMAIN][key].append(fnct)
@@ -123,7 +117,10 @@ def switch_ir_send_schema(
def get_child_schema(
- gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType, schema
+ gateway: BaseAsyncGateway,
+ child: ChildSensor,
+ value_type_name: ValueType,
+ schema: dict,
) -> vol.Schema:
"""Return a child schema."""
set_req = gateway.const.SetReq
@@ -142,7 +139,7 @@ def get_child_schema(
def invalid_msg(
gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
-):
+) -> str:
"""Return a message for an invalid child during schema validation."""
pres = gateway.const.Presentation
set_req = gateway.const.SetReq
@@ -176,11 +173,15 @@ def validate_child(
) -> defaultdict[str, list[DevId]]:
"""Validate a child. Returns a dict mapping hass platform names to list of DevId."""
validated: defaultdict[str, list[DevId]] = defaultdict(list)
- pres: IntEnum = gateway.const.Presentation
- set_req: IntEnum = gateway.const.SetReq
+ pres: type[IntEnum] = gateway.const.Presentation
+ set_req: type[IntEnum] = gateway.const.SetReq
child_type_name: SensorType | None = next(
(member.name for member in pres if member.value == child.type), None
)
+ if not child_type_name:
+ _LOGGER.warning("Child type %s is not supported", child.type)
+ return validated
+
value_types: set[int] = {value_type} if value_type else {*child.values}
value_type_names: set[ValueType] = {
member.name for member in set_req if member.value in value_types
@@ -199,7 +200,7 @@ def validate_child(
child_value_names: set[ValueType] = {
member.name for member in set_req if member.value in child.values
}
- v_names: set[ValueType] = platform_v_names & child_value_names
+ v_names = platform_v_names & child_value_names
for v_name in v_names:
child_schema_gen = SCHEMAS.get((platform, v_name), default_schema)
diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py
index aea99e3ee35..b08d94cebb0 100644
--- a/homeassistant/components/mysensors/light.py
+++ b/homeassistant/components/mysensors/light.py
@@ -1,4 +1,8 @@
"""Support for MySensors lights."""
+from __future__ import annotations
+
+from typing import Any
+
from homeassistant.components import mysensors
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -10,8 +14,6 @@ from homeassistant.components.light import (
SUPPORT_WHITE_VALUE,
LightEntity,
)
-from homeassistant.components.mysensors import on_unload
-from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, callback
@@ -20,6 +22,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.color as color_util
from homeassistant.util.color import rgb_hex_to_rgb_list
+from .const import MYSENSORS_DISCOVERY, DiscoveryInfo, SensorType
+from .device import MySensorsDevice
+from .helpers import on_unload
+
SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE
@@ -27,15 +33,15 @@ async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
-):
+) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway)."""
- device_class_map = {
+ device_class_map: dict[SensorType, type[MySensorsDevice]] = {
"S_DIMMER": MySensorsLightDimmer,
"S_RGB_LIGHT": MySensorsLightRGB,
"S_RGBW_LIGHT": MySensorsLightRGBW,
}
- async def async_discover(discovery_info):
+ async def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors light."""
mysensors.setup_mysensors_platform(
hass,
@@ -45,9 +51,9 @@ async def async_setup_entry(
async_add_entities=async_add_entities,
)
- await on_unload(
+ on_unload(
hass,
- config_entry,
+ config_entry.entry_id,
async_dispatcher_connect(
hass,
MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
@@ -59,35 +65,35 @@ async def async_setup_entry(
class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
"""Representation of a MySensors Light child node."""
- def __init__(self, *args):
+ def __init__(self, *args: Any) -> None:
"""Initialize a MySensors Light."""
super().__init__(*args)
- self._state = None
- self._brightness = None
- self._hs = None
- self._white = None
+ self._state: bool | None = None
+ self._brightness: int | None = None
+ self._hs: tuple[int, int] | None = None
+ self._white: int | None = None
@property
- def brightness(self):
+ def brightness(self) -> int | None:
"""Return the brightness of this light between 0..255."""
return self._brightness
@property
- def hs_color(self):
+ def hs_color(self) -> tuple[int, int] | None:
"""Return the hs color value [int, int]."""
return self._hs
@property
- def white_value(self):
+ def white_value(self) -> int | None:
"""Return the white value of this light between 0..255."""
return self._white
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return true if device is on."""
- return self._state
+ return bool(self._state)
- def _turn_on_light(self):
+ def _turn_on_light(self) -> None:
"""Turn on light child device."""
set_req = self.gateway.const.SetReq
@@ -102,10 +108,9 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
self._state = True
self._values[set_req.V_LIGHT] = STATE_ON
- def _turn_on_dimmer(self, **kwargs):
+ def _turn_on_dimmer(self, **kwargs: Any) -> None:
"""Turn on dimmer child device."""
set_req = self.gateway.const.SetReq
- brightness = self._brightness
if (
ATTR_BRIGHTNESS not in kwargs
@@ -113,7 +118,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
or set_req.V_DIMMER not in self._values
):
return
- brightness = kwargs[ATTR_BRIGHTNESS]
+ brightness: int = kwargs[ATTR_BRIGHTNESS]
percent = round(100 * brightness / 255)
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_DIMMER, percent, ack=1
@@ -124,17 +129,19 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
self._brightness = brightness
self._values[set_req.V_DIMMER] = percent
- def _turn_on_rgb_and_w(self, hex_template, **kwargs):
+ def _turn_on_rgb_and_w(self, hex_template: str, **kwargs: Any) -> None:
"""Turn on RGB or RGBW child device."""
+ assert self._hs
rgb = list(color_util.color_hs_to_RGB(*self._hs))
white = self._white
hex_color = self._values.get(self.value_type)
- hs_color = kwargs.get(ATTR_HS_COLOR)
+ hs_color: tuple[float, float] | None = kwargs.get(ATTR_HS_COLOR)
+ new_rgb: tuple[int, int, int] | None
if hs_color is not None:
new_rgb = color_util.color_hs_to_RGB(*hs_color)
else:
new_rgb = None
- new_white = kwargs.get(ATTR_WHITE_VALUE)
+ new_white: int | None = kwargs.get(ATTR_WHITE_VALUE)
if new_rgb is None and new_white is None:
return
@@ -143,8 +150,10 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
if hex_template == "%02x%02x%02x%02x":
if new_white is not None:
rgb.append(new_white)
- else:
+ elif white is not None:
rgb.append(white)
+ else:
+ rgb.append(0)
hex_color = hex_template % tuple(rgb)
if len(rgb) > 3:
white = rgb.pop()
@@ -154,11 +163,11 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
if self.assumed_state:
# optimistically assume that light has changed state
- self._hs = color_util.color_RGB_to_hs(*rgb)
+ self._hs = color_util.color_RGB_to_hs(*rgb) # type: ignore[assignment]
self._white = white
self._values[self.value_type] = hex_color
- async def async_turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
value_type = self.gateway.const.SetReq.V_LIGHT
self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0, ack=1)
@@ -169,13 +178,13 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
self.async_write_ha_state()
@callback
- def _async_update_light(self):
+ def _async_update_light(self) -> None:
"""Update the controller with values from light child."""
value_type = self.gateway.const.SetReq.V_LIGHT
self._state = self._values[value_type] == STATE_ON
@callback
- def _async_update_dimmer(self):
+ def _async_update_dimmer(self) -> None:
"""Update the controller with values from dimmer child."""
value_type = self.gateway.const.SetReq.V_DIMMER
if value_type in self._values:
@@ -184,31 +193,31 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
self._state = False
@callback
- def _async_update_rgb_or_w(self):
+ def _async_update_rgb_or_w(self) -> None:
"""Update the controller with values from RGB or RGBW child."""
value = self._values[self.value_type]
color_list = rgb_hex_to_rgb_list(value)
if len(color_list) > 3:
self._white = color_list.pop()
- self._hs = color_util.color_RGB_to_hs(*color_list)
+ self._hs = color_util.color_RGB_to_hs(*color_list) # type: ignore[assignment]
class MySensorsLightDimmer(MySensorsLight):
"""Dimmer child class to MySensorsLight."""
@property
- def supported_features(self):
+ def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_BRIGHTNESS
- async def async_turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
if self.assumed_state:
self.async_write_ha_state()
- async def async_update(self):
+ async def async_update(self) -> None:
"""Update the controller with the latest value from a sensor."""
await super().async_update()
self._async_update_light()
@@ -219,14 +228,14 @@ class MySensorsLightRGB(MySensorsLight):
"""RGB child class to MySensorsLight."""
@property
- def supported_features(self):
+ def supported_features(self) -> int:
"""Flag supported features."""
set_req = self.gateway.const.SetReq
if set_req.V_DIMMER in self._values:
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR
return SUPPORT_COLOR
- async def async_turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
@@ -234,7 +243,7 @@ class MySensorsLightRGB(MySensorsLight):
if self.assumed_state:
self.async_write_ha_state()
- async def async_update(self):
+ async def async_update(self) -> None:
"""Update the controller with the latest value from a sensor."""
await super().async_update()
self._async_update_light()
@@ -246,14 +255,14 @@ class MySensorsLightRGBW(MySensorsLightRGB):
"""RGBW child class to MySensorsLightRGB."""
@property
- def supported_features(self):
+ def supported_features(self) -> int:
"""Flag supported features."""
set_req = self.gateway.const.SetReq
if set_req.V_DIMMER in self._values:
return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW
return SUPPORT_MYSENSORS_RGBW
- async def async_turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
diff --git a/homeassistant/components/mysensors/notify.py b/homeassistant/components/mysensors/notify.py
index 50fca55ab39..109b357dee7 100644
--- a/homeassistant/components/mysensors/notify.py
+++ b/homeassistant/components/mysensors/notify.py
@@ -1,9 +1,20 @@
"""MySensors notification service."""
+from __future__ import annotations
+
+from typing import Any
+
from homeassistant.components import mysensors
from homeassistant.components.notify import ATTR_TARGET, DOMAIN, BaseNotificationService
+from homeassistant.core import HomeAssistant
+
+from .const import DevId, DiscoveryInfo
-async def async_get_service(hass, config, discovery_info=None):
+async def async_get_service(
+ hass: HomeAssistant,
+ config: dict[str, Any],
+ discovery_info: DiscoveryInfo | None = None,
+) -> BaseNotificationService | None:
"""Get the MySensors notification service."""
if not discovery_info:
return None
@@ -19,7 +30,7 @@ async def async_get_service(hass, config, discovery_info=None):
class MySensorsNotificationDevice(mysensors.device.MySensorsDevice):
"""Represent a MySensors Notification device."""
- def send_msg(self, msg):
+ def send_msg(self, msg: str) -> None:
"""Send a message."""
for sub_msg in [msg[i : i + 25] for i in range(0, len(msg), 25)]:
# Max mysensors payload is 25 bytes.
@@ -27,7 +38,7 @@ class MySensorsNotificationDevice(mysensors.device.MySensorsDevice):
self.node_id, self.child_id, self.value_type, sub_msg
)
- def __repr__(self):
+ def __repr__(self) -> str:
"""Return the representation."""
return f""
@@ -35,11 +46,15 @@ class MySensorsNotificationDevice(mysensors.device.MySensorsDevice):
class MySensorsNotificationService(BaseNotificationService):
"""Implement a MySensors notification service."""
- def __init__(self, hass):
+ def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the service."""
- self.devices = mysensors.get_mysensors_devices(hass, DOMAIN)
+ self.devices: dict[
+ DevId, MySensorsNotificationDevice
+ ] = mysensors.get_mysensors_devices(
+ hass, DOMAIN
+ ) # type: ignore[assignment]
- async def async_send_message(self, message="", **kwargs):
+ async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message to a user."""
target_devices = kwargs.get(ATTR_TARGET)
devices = [
diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py
index 48ab6e5d3a2..2ede5e38c6a 100644
--- a/homeassistant/components/mysensors/sensor.py
+++ b/homeassistant/components/mysensors/sensor.py
@@ -1,9 +1,9 @@
"""Support for MySensors sensors."""
+from __future__ import annotations
+
from awesomeversion import AwesomeVersion
from homeassistant.components import mysensors
-from homeassistant.components.mysensors import on_unload
-from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
from homeassistant.components.sensor import DOMAIN, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -27,7 +27,10 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-SENSORS = {
+from .const import MYSENSORS_DISCOVERY, DiscoveryInfo
+from .helpers import on_unload
+
+SENSORS: dict[str, list[str | None] | dict[str, list[str | None]]] = {
"V_TEMP": [None, "mdi:thermometer"],
"V_HUM": [PERCENTAGE, "mdi:water-percent"],
"V_DIMMER": [PERCENTAGE, "mdi:percent"],
@@ -66,10 +69,10 @@ async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
-):
+) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway)."""
- async def async_discover(discovery_info):
+ async def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors sensor."""
mysensors.setup_mysensors_platform(
hass,
@@ -79,9 +82,9 @@ async def async_setup_entry(
async_add_entities=async_add_entities,
)
- await on_unload(
+ on_unload(
hass,
- config_entry,
+ config_entry.entry_id,
async_dispatcher_connect(
hass,
MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
@@ -94,7 +97,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity):
"""Representation of a MySensors Sensor child node."""
@property
- def force_update(self):
+ def force_update(self) -> bool:
"""Return True if state updates should be forced.
If True, a state change will be triggered anytime the state property is
@@ -103,36 +106,43 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity):
return True
@property
- def state(self):
+ def state(self) -> str | None:
"""Return the state of the device."""
return self._values.get(self.value_type)
@property
- def icon(self):
+ def icon(self) -> str | None:
"""Return the icon to use in the frontend, if any."""
icon = self._get_sensor_type()[1]
return icon
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of this entity."""
set_req = self.gateway.const.SetReq
if (
AwesomeVersion(self.gateway.protocol_version) >= AwesomeVersion("1.5")
and set_req.V_UNIT_PREFIX in self._values
):
- return self._values[set_req.V_UNIT_PREFIX]
+ custom_unit: str = self._values[set_req.V_UNIT_PREFIX]
+ return custom_unit
+
+ if set_req(self.value_type) == set_req.V_TEMP:
+ if self.hass.config.units.is_metric:
+ return TEMP_CELSIUS
+ return TEMP_FAHRENHEIT
+
unit = self._get_sensor_type()[0]
return unit
- def _get_sensor_type(self):
+ def _get_sensor_type(self) -> list[str | None]:
"""Return list with unit and icon of sensor type."""
pres = self.gateway.const.Presentation
set_req = self.gateway.const.SetReq
- SENSORS[set_req.V_TEMP.name][0] = (
- TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT
- )
- sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None])
- if isinstance(sensor_type, dict):
- sensor_type = sensor_type.get(pres(self.child_type).name, [None, None])
+
+ _sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None])
+ if isinstance(_sensor_type, dict):
+ sensor_type = _sensor_type.get(pres(self.child_type).name, [None, None])
+ else:
+ sensor_type = _sensor_type
return sensor_type
diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py
index 32a6a9a1202..cdb4979d16b 100644
--- a/homeassistant/components/mysensors/switch.py
+++ b/homeassistant/components/mysensors/switch.py
@@ -1,17 +1,29 @@
"""Support for MySensors switches."""
+from __future__ import annotations
+
+from contextlib import suppress
+from typing import Any
+
import voluptuous as vol
from homeassistant.components import mysensors
from homeassistant.components.switch import DOMAIN, SwitchEntity
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
-from homeassistant.core import HomeAssistant
+from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import on_unload
from ...config_entries import ConfigEntry
from ...helpers.dispatcher import async_dispatcher_connect
-from .const import DOMAIN as MYSENSORS_DOMAIN, MYSENSORS_DISCOVERY, SERVICE_SEND_IR_CODE
+from .const import (
+ DOMAIN as MYSENSORS_DOMAIN,
+ MYSENSORS_DISCOVERY,
+ SERVICE_SEND_IR_CODE,
+ DiscoveryInfo,
+ SensorType,
+)
+from .device import MySensorsDevice
+from .helpers import on_unload
ATTR_IR_CODE = "V_IR_SEND"
@@ -24,9 +36,9 @@ async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
-):
+) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway)."""
- device_class_map = {
+ device_class_map: dict[SensorType, type[MySensorsDevice]] = {
"S_DOOR": MySensorsSwitch,
"S_MOTION": MySensorsSwitch,
"S_SMOKE": MySensorsSwitch,
@@ -42,7 +54,7 @@ async def async_setup_entry(
"S_WATER_QUALITY": MySensorsSwitch,
}
- async def async_discover(discovery_info):
+ async def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors switch."""
mysensors.setup_mysensors_platform(
hass,
@@ -52,7 +64,7 @@ async def async_setup_entry(
async_add_entities=async_add_entities,
)
- async def async_send_ir_code_service(service):
+ async def async_send_ir_code_service(service: ServiceCall) -> None:
"""Set IR code as device state attribute."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
ir_code = service.data.get(ATTR_IR_CODE)
@@ -83,9 +95,9 @@ async def async_setup_entry(
schema=SEND_IR_CODE_SERVICE_SCHEMA,
)
- await on_unload(
+ on_unload(
hass,
- config_entry,
+ config_entry.entry_id,
async_dispatcher_connect(
hass,
MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
@@ -98,17 +110,23 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
"""Representation of the value of a MySensors Switch child node."""
@property
- def current_power_w(self):
+ def current_power_w(self) -> float | None:
"""Return the current power usage in W."""
set_req = self.gateway.const.SetReq
- return self._values.get(set_req.V_WATT)
+ value = self._values.get(set_req.V_WATT)
+ float_value: float | None = None
+ if value is not None:
+ with suppress(ValueError):
+ float_value = float(value)
+
+ return float_value
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return True if switch is on."""
return self._values.get(self.value_type) == STATE_ON
- async def async_turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 1, ack=1
@@ -118,7 +136,7 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
self._values[self.value_type] = STATE_ON
self.async_write_ha_state()
- async def async_turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 0, ack=1
@@ -132,18 +150,18 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
class MySensorsIRSwitch(MySensorsSwitch):
"""IR switch child class to MySensorsSwitch."""
- def __init__(self, *args):
+ def __init__(self, *args: Any) -> None:
"""Set up instance attributes."""
super().__init__(*args)
- self._ir_code = None
+ self._ir_code: str | None = None
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return True if switch is on."""
set_req = self.gateway.const.SetReq
return self._values.get(set_req.V_LIGHT) == STATE_ON
- async def async_turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the IR switch on."""
set_req = self.gateway.const.SetReq
if ATTR_IR_CODE in kwargs:
@@ -162,7 +180,7 @@ class MySensorsIRSwitch(MySensorsSwitch):
# Turn off switch after switch was turned on
await self.async_turn_off()
- async def async_turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the IR switch off."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
@@ -173,7 +191,7 @@ class MySensorsIRSwitch(MySensorsSwitch):
self._values[set_req.V_LIGHT] = STATE_OFF
self.async_write_ha_state()
- async def async_update(self):
+ async def async_update(self) -> None:
"""Update the controller with the latest value from a sensor."""
await super().async_update()
self._ir_code = self._values.get(self.value_type)
diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json
index c61c4177136..bb6a1ed7bfe 100644
--- a/homeassistant/components/mysensors/translations/de.json
+++ b/homeassistant/components/mysensors/translations/de.json
@@ -15,24 +15,37 @@
"invalid_subscribe_topic": "Ung\u00fcltiges Abonnementthema",
"invalid_version": "Ung\u00fcltige MySensors Version",
"not_a_number": "Bitte eine Nummer eingeben",
+ "port_out_of_range": "Die Portnummer muss mindestens 1 und darf h\u00f6chstens 65535 sein",
+ "same_topic": "Themen zum Abonnieren und Ver\u00f6ffentlichen sind gleich",
"unknown": "Unerwarteter Fehler"
},
"error": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
"cannot_connect": "Verbindung fehlgeschlagen",
+ "duplicate_persistence_file": "Persistenzdatei wird bereits verwendet",
+ "duplicate_topic": "Thema bereits in Verwendung",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"invalid_device": "Ung\u00fcltiges Ger\u00e4t",
"invalid_ip": "Ung\u00fcltige IP-Adresse",
+ "invalid_persistence_file": "Ung\u00fcltige Persistenzdatei",
+ "invalid_port": "Ung\u00fcltige Portnummer",
+ "invalid_publish_topic": "Ung\u00fcltiges Ver\u00f6ffentlichungsthema",
"invalid_serial": "Ung\u00fcltiger Serieller Port",
+ "invalid_subscribe_topic": "Ung\u00fcltiges Abonnementthema",
"invalid_version": "Ung\u00fcltige MySensors Version",
"mqtt_required": "Die MQTT-Integration ist nicht eingerichtet",
"not_a_number": "Bitte eine Nummer eingeben",
+ "port_out_of_range": "Die Portnummer muss mindestens 1 und darf h\u00f6chstens 65535 sein",
+ "same_topic": "Themen zum Abonnieren und Ver\u00f6ffentlichen sind gleich",
"unknown": "Unerwarteter Fehler"
},
"step": {
"gw_mqtt": {
"data": {
+ "persistence_file": "Persistenzdatei (leer lassen, um automatisch zu generieren)",
"retain": "MQTT behalten",
+ "topic_in_prefix": "Pr\u00e4fix f\u00fcr Eingabethemen (topic_in_prefix)",
+ "topic_out_prefix": "Pr\u00e4fix f\u00fcr Ausgabethemen (topic_out_prefix)",
"version": "MySensors Version"
},
"description": "MQTT-Gateway einrichten"
@@ -41,6 +54,7 @@
"data": {
"baud_rate": "Baudrate",
"device": "Serielle Schnittstelle",
+ "persistence_file": "Persistenzdatei (leer lassen, um automatisch zu generieren)",
"version": "MySensors Version"
},
"description": "Einrichtung des seriellen Gateways"
@@ -48,6 +62,7 @@
"gw_tcp": {
"data": {
"device": "IP-Adresse des Gateways",
+ "persistence_file": "Persistenzdatei (leer lassen, um automatisch zu generieren)",
"tcp_port": "Port",
"version": "MySensors Version"
},
diff --git a/homeassistant/components/mysensors/translations/he.json b/homeassistant/components/mysensors/translations/he.json
new file mode 100644
index 00000000000..7ded555edb8
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/he.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\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",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\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",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nad/manifest.json b/homeassistant/components/nad/manifest.json
index 063ceca0fd7..59d82acddf2 100644
--- a/homeassistant/components/nad/manifest.json
+++ b/homeassistant/components/nad/manifest.json
@@ -2,7 +2,7 @@
"domain": "nad",
"name": "NAD",
"documentation": "https://www.home-assistant.io/integrations/nad",
- "requirements": ["nad_receiver==0.0.12"],
+ "requirements": ["nad_receiver==0.2.0"],
"codeowners": [],
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py
index 7dc6701217d..97d54fb0669 100644
--- a/homeassistant/components/nam/__init__.py
+++ b/homeassistant/components/nam/__init__.py
@@ -9,24 +9,33 @@ from aiohttp.client_exceptions import ClientConnectorError
import async_timeout
from nettigo_air_monitor import (
ApiError,
- DictToObj,
InvalidSensorData,
+ NAMSensors,
NettigoAirMonitor,
)
+from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-from .const import DEFAULT_NAME, DEFAULT_UPDATE_INTERVAL, DOMAIN, MANUFACTURER
+from .const import (
+ ATTR_SDS011,
+ ATTR_SPS30,
+ DEFAULT_NAME,
+ DEFAULT_UPDATE_INTERVAL,
+ DOMAIN,
+ MANUFACTURER,
+)
_LOGGER = logging.getLogger(__name__)
-PLATFORMS = ["air_quality", "sensor"]
+PLATFORMS = ["sensor"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -43,6 +52,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+ # Remove air_quality entities from registry if they exist
+ ent_reg = entity_registry.async_get(hass)
+ for sensor_type in ["sds", ATTR_SDS011, ATTR_SPS30]:
+ unique_id = f"{coordinator.unique_id}-{sensor_type}"
+ if entity_id := ent_reg.async_get_entity_id(
+ AIR_QUALITY_PLATFORM, DOMAIN, unique_id
+ ):
+ _LOGGER.debug("Removing deprecated air_quality entity %s", entity_id)
+ ent_reg.async_remove(entity_id)
+
return True
@@ -75,7 +94,7 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator):
hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL
)
- async def _async_update_data(self) -> DictToObj:
+ async def _async_update_data(self) -> NAMSensors:
"""Update data via library."""
try:
# Device firmware uses synchronous code and doesn't respond to http queries
@@ -86,8 +105,6 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator):
except (ApiError, ClientConnectorError, InvalidSensorData) as error:
raise UpdateFailed(error) from error
- _LOGGER.debug(data)
-
return data
@property
diff --git a/homeassistant/components/nam/air_quality.py b/homeassistant/components/nam/air_quality.py
deleted file mode 100644
index 163b50148db..00000000000
--- a/homeassistant/components/nam/air_quality.py
+++ /dev/null
@@ -1,103 +0,0 @@
-"""Support for the Nettigo Air Monitor air_quality service."""
-from __future__ import annotations
-
-from homeassistant.components.air_quality import AirQualityEntity
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity import DeviceInfo
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import StateType
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from . import NAMDataUpdateCoordinator
-from .const import (
- AIR_QUALITY_SENSORS,
- ATTR_MHZ14A_CARBON_DIOXIDE,
- DEFAULT_NAME,
- DOMAIN,
- SUFFIX_P1,
- SUFFIX_P2,
-)
-
-PARALLEL_UPDATES = 1
-
-
-async def async_setup_entry(
- hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
-) -> None:
- """Add a Nettigo Air Monitor entities from a config_entry."""
- coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
-
- entities: list[NAMAirQuality] = []
- for sensor in AIR_QUALITY_SENSORS:
- if f"{sensor}{SUFFIX_P1}" in coordinator.data:
- entities.append(NAMAirQuality(coordinator, sensor))
-
- async_add_entities(entities, False)
-
-
-class NAMAirQuality(CoordinatorEntity, AirQualityEntity):
- """Define an Nettigo Air Monitor air quality."""
-
- coordinator: NAMDataUpdateCoordinator
-
- def __init__(self, coordinator: NAMDataUpdateCoordinator, sensor_type: str) -> None:
- """Initialize."""
- super().__init__(coordinator)
- self.sensor_type = sensor_type
-
- @property
- def name(self) -> str:
- """Return the name."""
- return f"{DEFAULT_NAME} {AIR_QUALITY_SENSORS[self.sensor_type]}"
-
- @property
- def particulate_matter_2_5(self) -> StateType:
- """Return the particulate matter 2.5 level."""
- return round_state(
- getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}")
- )
-
- @property
- def particulate_matter_10(self) -> StateType:
- """Return the particulate matter 10 level."""
- return round_state(
- getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P1}")
- )
-
- @property
- def carbon_dioxide(self) -> StateType:
- """Return the particulate matter 10 level."""
- return round_state(
- getattr(self.coordinator.data, ATTR_MHZ14A_CARBON_DIOXIDE, None)
- )
-
- @property
- def unique_id(self) -> str:
- """Return a unique_id for this entity."""
- return f"{self.coordinator.unique_id}-{self.sensor_type}"
-
- @property
- def device_info(self) -> DeviceInfo:
- """Return the device info."""
- return self.coordinator.device_info
-
- @property
- def available(self) -> bool:
- """Return if entity is available."""
- available = super().available
-
- # For a short time after booting, the device does not return values for all
- # sensors. For this reason, we mark entities for which data is missing as
- # unavailable.
- return available and bool(
- getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}", None)
- )
-
-
-def round_state(state: StateType) -> StateType:
- """Round state."""
- if isinstance(state, float):
- return round(state)
-
- return state
diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py
index ccb5e6e6e84..a44f3f2ba6a 100644
--- a/homeassistant/components/nam/config_flow.py
+++ b/homeassistant/components/nam/config_flow.py
@@ -118,4 +118,4 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
# when reading data from sensors. The nettigo-air-monitor library tries to get
# the data 4 times, so we use a longer than usual timeout here.
with async_timeout.timeout(30):
- return cast(str, await nam.async_get_mac_address())
+ return await nam.async_get_mac_address()
diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py
index 1c191019c04..f60d03eea78 100644
--- a/homeassistant/components/nam/const.py
+++ b/homeassistant/components/nam/const.py
@@ -9,6 +9,8 @@ from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ CONCENTRATION_PARTS_PER_MILLION,
+ DEVICE_CLASS_CO2,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_SIGNAL_STRENGTH,
@@ -22,21 +24,32 @@ from homeassistant.const import (
from .model import SensorDescription
+SUFFIX_P0: Final = "_p0"
+SUFFIX_P1: Final = "_p1"
+SUFFIX_P2: Final = "_p2"
+SUFFIX_P4: Final = "_p4"
+
ATTR_BME280_HUMIDITY: Final = "bme280_humidity"
ATTR_BME280_PRESSURE: Final = "bme280_pressure"
ATTR_BME280_TEMPERATURE: Final = "bme280_temperature"
ATTR_BMP280_PRESSURE: Final = "bmp280_pressure"
ATTR_BMP280_TEMPERATURE: Final = "bmp280_temperature"
-ATTR_DHT22_HUMIDITY: Final = "humidity"
-ATTR_DHT22_TEMPERATURE: Final = "temperature"
+ATTR_DHT22_HUMIDITY: Final = "dht22_humidity"
+ATTR_DHT22_TEMPERATURE: Final = "dht22_temperature"
ATTR_HECA_HUMIDITY: Final = "heca_humidity"
ATTR_HECA_TEMPERATURE: Final = "heca_temperature"
-ATTR_MHZ14A_CARBON_DIOXIDE: Final = "conc_co2_ppm"
+ATTR_MHZ14A_CARBON_DIOXIDE: Final = "mhz14a_carbon_dioxide"
+ATTR_SDS011: Final = "sds011"
+ATTR_SDS011_P1: Final = f"{ATTR_SDS011}{SUFFIX_P1}"
+ATTR_SDS011_P2: Final = f"{ATTR_SDS011}{SUFFIX_P2}"
ATTR_SHT3X_HUMIDITY: Final = "sht3x_humidity"
ATTR_SHT3X_TEMPERATURE: Final = "sht3x_temperature"
ATTR_SIGNAL_STRENGTH: Final = "signal"
-ATTR_SPS30_P0: Final = "sps30_p0"
-ATTR_SPS30_P4: Final = "sps30_p4"
+ATTR_SPS30: Final = "sps30"
+ATTR_SPS30_P0: Final = f"{ATTR_SPS30}{SUFFIX_P0}"
+ATTR_SPS30_P1: Final = f"{ATTR_SPS30}{SUFFIX_P1}"
+ATTR_SPS30_P2: Final = f"{ATTR_SPS30}{SUFFIX_P2}"
+ATTR_SPS30_P4: Final = f"{ATTR_SPS30}{SUFFIX_P4}"
ATTR_UPTIME: Final = "uptime"
ATTR_ENABLED: Final = "enabled"
@@ -48,10 +61,10 @@ DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=6)
DOMAIN: Final = "nam"
MANUFACTURER: Final = "Nettigo"
-SUFFIX_P1: Final = "_p1"
-SUFFIX_P2: Final = "_p2"
-
-AIR_QUALITY_SENSORS: Final[dict[str, str]] = {"sds": "SDS011", "sps30": "SPS30"}
+MIGRATION_SENSORS: Final = [
+ ("temperature", ATTR_DHT22_TEMPERATURE),
+ ("humidity", ATTR_DHT22_HUMIDITY),
+]
SENSORS: Final[dict[str, SensorDescription]] = {
ATTR_BME280_HUMIDITY: {
@@ -110,6 +123,30 @@ SENSORS: Final[dict[str, SensorDescription]] = {
ATTR_ENABLED: True,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
+ ATTR_MHZ14A_CARBON_DIOXIDE: {
+ ATTR_LABEL: f"{DEFAULT_NAME} MH-Z14A Carbon Dioxide",
+ ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_CO2,
+ ATTR_ICON: None,
+ ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ },
+ ATTR_SDS011_P1: {
+ ATTR_LABEL: f"{DEFAULT_NAME} SDS011 Particulate Matter 10",
+ ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ ATTR_DEVICE_CLASS: None,
+ ATTR_ICON: "mdi:blur",
+ ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ },
+ ATTR_SDS011_P2: {
+ ATTR_LABEL: f"{DEFAULT_NAME} SDS011 Particulate Matter 2.5",
+ ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ ATTR_DEVICE_CLASS: None,
+ ATTR_ICON: "mdi:blur",
+ ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ },
ATTR_SHT3X_HUMIDITY: {
ATTR_LABEL: f"{DEFAULT_NAME} SHT3X Humidity",
ATTR_UNIT: PERCENTAGE,
@@ -134,6 +171,22 @@ SENSORS: Final[dict[str, SensorDescription]] = {
ATTR_ENABLED: True,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
+ ATTR_SPS30_P1: {
+ ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 10",
+ ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ ATTR_DEVICE_CLASS: None,
+ ATTR_ICON: "mdi:blur",
+ ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ },
+ ATTR_SPS30_P2: {
+ ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 2.5",
+ ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ ATTR_DEVICE_CLASS: None,
+ ATTR_ICON: "mdi:blur",
+ ATTR_ENABLED: True,
+ ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
+ },
ATTR_SPS30_P4: {
ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0",
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json
index 3e03a0ad787..a1401c485de 100644
--- a/homeassistant/components/nam/manifest.json
+++ b/homeassistant/components/nam/manifest.json
@@ -3,9 +3,18 @@
"name": "Nettigo Air Monitor",
"documentation": "https://www.home-assistant.io/integrations/nam",
"codeowners": ["@bieniu"],
- "requirements": ["nettigo-air-monitor==0.2.6"],
- "zeroconf": [{"type": "_http._tcp.local.", "name": "nam-*"}],
+ "requirements": ["nettigo-air-monitor==1.0.0"],
+ "zeroconf": [
+ {
+ "type": "_http._tcp.local.",
+ "name": "nam-*"
+ },
+ {
+ "type": "_http._tcp.local.",
+ "manufacturer": "nettigo"
+ }
+ ],
"config_flow": true,
"quality_scale": "platinum",
"iot_class": "local_polling"
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py
index 30982d8571d..ae3d1e639d5 100644
--- a/homeassistant/components/nam/sensor.py
+++ b/homeassistant/components/nam/sensor.py
@@ -2,22 +2,38 @@
from __future__ import annotations
from datetime import timedelta
-from typing import Any
+import logging
+from typing import cast
-from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity
+from homeassistant.components.sensor import (
+ ATTR_STATE_CLASS,
+ DOMAIN as PLATFORM,
+ SensorEntity,
+)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON
from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers import entity_registry
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utcnow
from . import NAMDataUpdateCoordinator
-from .const import ATTR_ENABLED, ATTR_LABEL, ATTR_UNIT, ATTR_UPTIME, DOMAIN, SENSORS
+from .const import (
+ ATTR_ENABLED,
+ ATTR_LABEL,
+ ATTR_UNIT,
+ ATTR_UPTIME,
+ DOMAIN,
+ MIGRATION_SENSORS,
+ SENSORS,
+)
PARALLEL_UPDATES = 1
+_LOGGER = logging.getLogger(__name__)
+
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@@ -25,9 +41,24 @@ async def async_setup_entry(
"""Add a Nettigo Air Monitor entities from a config_entry."""
coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+ # Due to the change of the attribute name of two sensors, it is necessary to migrate
+ # the unique_ids to the new names.
+ ent_reg = entity_registry.async_get(hass)
+ for old_sensor, new_sensor in MIGRATION_SENSORS:
+ old_unique_id = f"{coordinator.unique_id}-{old_sensor}"
+ new_unique_id = f"{coordinator.unique_id}-{new_sensor}"
+ if entity_id := ent_reg.async_get_entity_id(PLATFORM, DOMAIN, old_unique_id):
+ _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)
+
sensors: list[NAMSensor | NAMSensorUptime] = []
for sensor in SENSORS:
- if sensor in coordinator.data:
+ if getattr(coordinator.data, sensor) is not None:
if sensor == ATTR_UPTIME:
sensors.append(NAMSensorUptime(coordinator, sensor))
else:
@@ -44,49 +75,21 @@ class NAMSensor(CoordinatorEntity, SensorEntity):
def __init__(self, coordinator: NAMDataUpdateCoordinator, sensor_type: str) -> None:
"""Initialize."""
super().__init__(coordinator)
+ description = SENSORS[sensor_type]
+ self._attr_device_class = description[ATTR_DEVICE_CLASS]
+ self._attr_device_info = coordinator.device_info
+ self._attr_entity_registry_enabled_default = description[ATTR_ENABLED]
+ self._attr_icon = description[ATTR_ICON]
+ self._attr_name = description[ATTR_LABEL]
+ self._attr_state_class = description[ATTR_STATE_CLASS]
+ self._attr_unique_id = f"{coordinator.unique_id}-{sensor_type}"
+ self._attr_unit_of_measurement = description[ATTR_UNIT]
self.sensor_type = sensor_type
- self._description = SENSORS[sensor_type]
- self._attr_state_class = SENSORS[sensor_type][ATTR_STATE_CLASS]
@property
- def name(self) -> str:
- """Return the name."""
- return self._description[ATTR_LABEL]
-
- @property
- def state(self) -> Any:
+ def state(self) -> StateType:
"""Return the state."""
- return getattr(self.coordinator.data, self.sensor_type)
-
- @property
- def unit_of_measurement(self) -> str | None:
- """Return the unit the value is expressed in."""
- return self._description[ATTR_UNIT]
-
- @property
- def device_class(self) -> str | None:
- """Return the class of this sensor."""
- return self._description[ATTR_DEVICE_CLASS]
-
- @property
- def icon(self) -> str | None:
- """Return the icon."""
- return self._description[ATTR_ICON]
-
- @property
- def entity_registry_enabled_default(self) -> bool:
- """Return if the entity should be enabled when first added to the entity registry."""
- return self._description[ATTR_ENABLED]
-
- @property
- def unique_id(self) -> str:
- """Return a unique_id for this entity."""
- return f"{self.coordinator.unique_id}-{self.sensor_type}"
-
- @property
- def device_info(self) -> DeviceInfo:
- """Return the device info."""
- return self.coordinator.device_info
+ return cast(StateType, getattr(self.coordinator.data, self.sensor_type))
@property
def available(self) -> bool:
@@ -96,8 +99,8 @@ class NAMSensor(CoordinatorEntity, SensorEntity):
# For a short time after booting, the device does not return values for all
# sensors. For this reason, we mark entities for which data is missing as
# unavailable.
- return available and bool(
- getattr(self.coordinator.data, self.sensor_type, None)
+ return (
+ available and getattr(self.coordinator.data, self.sensor_type) is not None
)
diff --git a/homeassistant/components/nam/translations/de.json b/homeassistant/components/nam/translations/de.json
index 823a9675726..e3c7159a0d6 100644
--- a/homeassistant/components/nam/translations/de.json
+++ b/homeassistant/components/nam/translations/de.json
@@ -16,7 +16,8 @@
"user": {
"data": {
"host": "Host"
- }
+ },
+ "description": "Einrichten der Nettigo Air Monitor Integration."
}
}
}
diff --git a/homeassistant/components/nam/translations/he.json b/homeassistant/components/nam/translations/he.json
new file mode 100644
index 00000000000..39f3a7e4306
--- /dev/null
+++ b/homeassistant/components/nam/translations/he.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "flow_title": "{name}",
+ "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}?"
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json
index 7632360d13c..014e366db46 100644
--- a/homeassistant/components/neato/manifest.json
+++ b/homeassistant/components/neato/manifest.json
@@ -3,7 +3,7 @@
"name": "Neato Botvac",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/neato",
- "requirements": ["pybotvac==0.0.20"],
+ "requirements": ["pybotvac==0.0.21"],
"codeowners": ["@dshokouhi", "@Santobert"],
"dependencies": ["http"],
"iot_class": "cloud_polling"
diff --git a/homeassistant/components/neato/translations/he.json b/homeassistant/components/neato/translations/he.json
new file mode 100644
index 00000000000..876a9eded8b
--- /dev/null
+++ b/homeassistant/components/neato/translations/he.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+ "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "create_entry": {
+ "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ },
+ "reauth_confirm": {
+ "title": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py
index 5cd84effbc8..fb488763750 100644
--- a/homeassistant/components/nest/__init__.py
+++ b/homeassistant/components/nest/__init__.py
@@ -135,7 +135,7 @@ class SignalUpdateCallback:
self._hass.bus.async_fire(NEST_EVENT, message)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Nest from a config entry with dispatch between old/new flows."""
if DATA_SDM not in entry.data:
diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py
index d59ec05c503..4ed492a15fa 100644
--- a/homeassistant/components/nest/device_trigger.py
+++ b/homeassistant/components/nest/device_trigger.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.device_automation.exceptions import (
InvalidDeviceAutomationConfig,
)
@@ -20,7 +20,7 @@ DEVICE = "device"
TRIGGER_TYPES = set(DEVICE_TRAIT_TRIGGER_MAP.values())
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
}
diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json
index 6f43df5ac81..91274d8b731 100644
--- a/homeassistant/components/nest/translations/he.json
+++ b/homeassistant/components/nest/translations/he.json
@@ -1,10 +1,18 @@
{
"config": {
"abort": {
- "authorize_url_timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e6\u05d9\u05e8\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea"
+ "authorize_url_timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e6\u05d9\u05e8\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+ "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7",
+ "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."
+ },
+ "create_entry": {
+ "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
},
"error": {
"internal_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05e4\u05e0\u05d9\u05de\u05d9\u05ea \u05d1\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3",
+ "invalid_pin": "\u05e7\u05d5\u05d3 PIN \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
"timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3",
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4 \u05d1\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3"
},
@@ -13,15 +21,22 @@
"data": {
"flow_impl": "\u05e1\u05e4\u05e7"
},
- "description": "\u05d1\u05d7\u05e8 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05e4\u05e7 \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d0\u05de\u05ea \u05e2\u05dd Nest.",
+ "description": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea",
"title": "\u05e1\u05e4\u05e7 \u05d0\u05d9\u05de\u05d5\u05ea"
},
"link": {
"data": {
- "code": "\u05e7\u05d5\u05d3 Pin"
+ "code": "\u05e7\u05d5\u05d3 PIN"
},
"description": "\u05db\u05d3\u05d9 \u05dc\u05e7\u05e9\u05e8 \u05d0\u05ea \u05d7\u05e9\u05d1\u05d5\u05df Nest \u05e9\u05dc\u05da, [\u05d0\u05de\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da] ({url}). \n\n \u05dc\u05d0\u05d7\u05e8 \u05d4\u05d0\u05d9\u05e9\u05d5\u05e8, \u05d4\u05e2\u05ea\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4PIN \u05e9\u05e1\u05d5\u05e4\u05e7 \u05d5\u05d4\u05d3\u05d1\u05e7 \u05d0\u05d5\u05ea\u05d5 \u05dc\u05de\u05d8\u05d4.",
"title": "\u05e7\u05d9\u05e9\u05d5\u05e8 \u05d7\u05e9\u05d1\u05d5\u05df Nest"
+ },
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ },
+ "reauth_confirm": {
+ "description": "\u05e9\u05d9\u05dc\u05d5\u05d1 Nest \u05e6\u05e8\u05d9\u05da \u05dc\u05d0\u05de\u05ea \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da",
+ "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1"
}
}
}
diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json
index 53066e9e720..a849b76f1c9 100644
--- a/homeassistant/components/nest/translations/nl.json
+++ b/homeassistant/components/nest/translations/nl.json
@@ -36,7 +36,7 @@
"title": "Kies een authenticatie methode"
},
"reauth_confirm": {
- "description": "De Nest-integratie moet je account opnieuw verifi\u00ebren",
+ "description": "De Nest-integratie moet uw account opnieuw verifi\u00ebren",
"title": "Verifieer de integratie opnieuw"
}
}
diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py
index f6805099211..d92e50107c9 100644
--- a/homeassistant/components/netatmo/__init__.py
+++ b/homeassistant/components/netatmo/__init__.py
@@ -96,7 +96,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Netatmo from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
@@ -131,7 +131,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
{"type": "None", "data": {WEBHOOK_PUSH_TYPE: WEBHOOK_DEACTIVATION}},
)
webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
- await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook()
+ try:
+ await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook()
+ except pyatmo.ApiError:
+ _LOGGER.debug(
+ "No webhook to be dropped for %s", entry.data[CONF_WEBHOOK_ID]
+ )
async def register_webhook(event):
if CONF_WEBHOOK_ID not in entry.data:
diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py
index d6085ec06ec..b0d4e18b7c9 100644
--- a/homeassistant/components/netatmo/device_trigger.py
+++ b/homeassistant/components/netatmo/device_trigger.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.device_automation.exceptions import (
InvalidDeviceAutomationConfig,
)
@@ -54,7 +54,7 @@ SUBTYPES = {
TRIGGER_TYPES = OUTDOOR_CAMERA_TRIGGERS + INDOOR_CAMERA_TRIGGERS + CLIMATE_TRIGGERS
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json
index 60a54df8a6e..de7fbc36038 100644
--- a/homeassistant/components/netatmo/manifest.json
+++ b/homeassistant/components/netatmo/manifest.json
@@ -3,7 +3,7 @@
"name": "Netatmo",
"documentation": "https://www.home-assistant.io/integrations/netatmo",
"requirements": [
- "pyatmo==5.0.1"
+ "pyatmo==5.2.0"
],
"after_dependencies": [
"cloud",
diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json
index 1037d100909..73106797381 100644
--- a/homeassistant/components/netatmo/translations/de.json
+++ b/homeassistant/components/netatmo/translations/de.json
@@ -49,6 +49,7 @@
"mode": "Berechnung",
"show_on_map": "Auf Karte anzeigen"
},
+ "description": "Konfigurieren Sie einen \u00f6ffentlichen Wettersensor f\u00fcr einen Bereich.",
"title": "\u00d6ffentlicher Netatmo Wettersensor"
},
"public_weather_areas": {
diff --git a/homeassistant/components/netatmo/translations/he.json b/homeassistant/components/netatmo/translations/he.json
index 54bef84c30a..814a0093b2c 100644
--- a/homeassistant/components/netatmo/translations/he.json
+++ b/homeassistant/components/netatmo/translations/he.json
@@ -1,11 +1,30 @@
{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+ "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})",
+ "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."
+ },
+ "create_entry": {
+ "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ }
+ }
+ },
"device_automation": {
+ "trigger_subtype": {
+ "away": "\u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea"
+ },
"trigger_type": {
- "animal": "\u05d6\u05d9\u05d4\u05d4 \u05d1\u05e2\u05dc-\u05d7\u05d9\u05d9\u05dd",
- "human": "\u05d6\u05d9\u05d4\u05d4 \u05d0\u05d3\u05dd",
- "movement": "\u05d6\u05d9\u05d4\u05d4 \u05ea\u05e0\u05d5\u05e2\u05d4",
- "turned_off": "\u05db\u05d1\u05d4",
- "turned_on": "\u05e0\u05d3\u05dc\u05e7"
+ "animal": "{entity_name} \u05d6\u05d9\u05d4\u05d4 \u05d1\u05e2\u05dc \u05d7\u05d9\u05d9\u05dd",
+ "human": "{entity_name} \u05d6\u05d9\u05d4\u05d4 \u05d0\u05d3\u05dd",
+ "movement": "\u05ea\u05e0\u05d5\u05e2\u05d4 \u05d6\u05d5\u05d4\u05ea\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 {entity_name}",
+ "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4",
+ "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/network/network.py b/homeassistant/components/network/network.py
index 1243ba24774..ffe3406e28e 100644
--- a/homeassistant/components/network/network.py
+++ b/homeassistant/components/network/network.py
@@ -8,7 +8,6 @@ from homeassistant.core import HomeAssistant, callback
from .const import (
ATTR_CONFIGURED_ADAPTERS,
DEFAULT_CONFIGURED_ADAPTERS,
- NETWORK_CONFIG_SCHEMA,
STORAGE_KEY,
STORAGE_VERSION,
)
@@ -63,7 +62,6 @@ class Network:
async def async_reconfig(self, config: dict[str, Any]) -> None:
"""Reconfigure network."""
- config = NETWORK_CONFIG_SCHEMA(config)
self._data[ATTR_CONFIGURED_ADAPTERS] = config[ATTR_CONFIGURED_ADAPTERS]
self.async_configure()
await self._async_save()
diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py
index 2bc17fbecb2..d74d6338c8b 100644
--- a/homeassistant/components/neurio_energy/sensor.py
+++ b/homeassistant/components/neurio_energy/sensor.py
@@ -6,7 +6,11 @@ import neurio
import requests.exceptions
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.components.sensor import (
+ PLATFORM_SCHEMA,
+ STATE_CLASS_MEASUREMENT,
+ SensorEntity,
+)
from homeassistant.const import CONF_API_KEY, ENERGY_KILO_WATT_HOUR, POWER_WATT
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
@@ -135,6 +139,7 @@ class NeurioEnergy(SensorEntity):
if sensor_type == ACTIVE_TYPE:
self._unit_of_measurement = POWER_WATT
+ self._attr_state_class = STATE_CLASS_MEASUREMENT
elif sensor_type == DAILY_TYPE:
self._unit_of_measurement = ENERGY_KILO_WATT_HOUR
diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py
index 65be22b57f1..e9f31749042 100644
--- a/homeassistant/components/nexia/__init__.py
+++ b/homeassistant/components/nexia/__init__.py
@@ -24,7 +24,7 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN)
DEFAULT_UPDATE_RATE = 120
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Configure the base Nexia device for Home Assistant."""
conf = entry.data
diff --git a/homeassistant/components/nexia/translations/de.json b/homeassistant/components/nexia/translations/de.json
index f2220f828e8..e2a9a9bc739 100644
--- a/homeassistant/components/nexia/translations/de.json
+++ b/homeassistant/components/nexia/translations/de.json
@@ -11,6 +11,7 @@
"step": {
"user": {
"data": {
+ "brand": "Marke",
"password": "Passwort",
"username": "Benutzername"
},
diff --git a/homeassistant/components/nexia/translations/es.json b/homeassistant/components/nexia/translations/es.json
index d2d92d1e85b..1698b8db1d1 100644
--- a/homeassistant/components/nexia/translations/es.json
+++ b/homeassistant/components/nexia/translations/es.json
@@ -11,6 +11,7 @@
"step": {
"user": {
"data": {
+ "brand": "Marca",
"password": "Contrase\u00f1a",
"username": "Usuario"
},
diff --git a/homeassistant/components/nexia/translations/he.json b/homeassistant/components/nexia/translations/he.json
index ac90b3264ea..7f9efa5b24e 100644
--- a/homeassistant/components/nexia/translations/he.json
+++ b/homeassistant/components/nexia/translations/he.json
@@ -1,11 +1,21 @@
{
"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",
+ "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": {
+ "brand": "\u05de\u05d5\u05ea\u05d2",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
- }
+ },
+ "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc-mynexia.com"
}
}
}
diff --git a/homeassistant/components/nexia/translations/pl.json b/homeassistant/components/nexia/translations/pl.json
index fc1bdf7f273..6e40e21a7f0 100644
--- a/homeassistant/components/nexia/translations/pl.json
+++ b/homeassistant/components/nexia/translations/pl.json
@@ -11,6 +11,7 @@
"step": {
"user": {
"data": {
+ "brand": "Marka",
"password": "Has\u0142o",
"username": "Nazwa u\u017cytkownika"
},
diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py
index 67d0a4a81d7..fb03bcd25b5 100644
--- a/homeassistant/components/nextbus/sensor.py
+++ b/homeassistant/components/nextbus/sensor.py
@@ -18,8 +18,6 @@ CONF_AGENCY = "agency"
CONF_ROUTE = "route"
CONF_STOP = "stop"
-ICON = "mdi:bus"
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_AGENCY): cv.string,
@@ -114,6 +112,9 @@ class NextBusDepartureSensor(SensorEntity):
the future using fuzzy logic and matching.
"""
+ _attr_device_class = DEVICE_CLASS_TIMESTAMP
+ _attr_icon = "mdi:bus"
+
def __init__(self, client, agency, route, stop, name=None):
"""Initialize sensor with all required config."""
self.agency = agency
@@ -144,11 +145,6 @@ class NextBusDepartureSensor(SensorEntity):
return self._name
- @property
- def device_class(self):
- """Return the device class."""
- return DEVICE_CLASS_TIMESTAMP
-
@property
def state(self):
"""Return current state of the sensor."""
@@ -159,13 +155,6 @@ class NextBusDepartureSensor(SensorEntity):
"""Return additional state attributes."""
return self._attributes
- @property
- def icon(self):
- """Return icon to be used for this sensor."""
- # Would be nice if we could determine if the line is a train or bus
- # however that doesn't seem to be available to us. Using bus for now.
- return ICON
-
def update(self):
"""Update sensor with new departures times."""
# Note: using Multi because there is a bug with the single stop impl
diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py
index 8608386c483..69d79d1cecb 100644
--- a/homeassistant/components/nightscout/__init__.py
+++ b/homeassistant/components/nightscout/__init__.py
@@ -18,7 +18,7 @@ PLATFORMS = ["sensor"]
_API_TIMEOUT = SLOW_UPDATE_WARNING - 1
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Nightscout from a config entry."""
server_url = entry.data[CONF_URL]
api_key = entry.data.get(CONF_API_KEY)
diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json
index 510d57ce45f..91461416c60 100644
--- a/homeassistant/components/nightscout/translations/de.json
+++ b/homeassistant/components/nightscout/translations/de.json
@@ -14,7 +14,9 @@
"data": {
"api_key": "API-Schl\u00fcssel",
"url": "URL"
- }
+ },
+ "description": "- URL: die Adresse Ihrer Nightscout-Instanz. Z.B.: https://myhomeassistant.duckdns.org:5423\n- API-Schl\u00fcssel (Optional): Nur verwenden, wenn Ihre Instanz gesch\u00fctzt ist (auth_default_roles != readable).",
+ "title": "Geben Sie Ihre Nightscout-Serverinformationen ein."
}
}
}
diff --git a/homeassistant/components/nightscout/translations/he.json b/homeassistant/components/nightscout/translations/he.json
new file mode 100644
index 00000000000..eebb1cc0a93
--- /dev/null
+++ b/homeassistant/components/nightscout/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"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nmap_tracker/const.py b/homeassistant/components/nmap_tracker/const.py
new file mode 100644
index 00000000000..88118a81811
--- /dev/null
+++ b/homeassistant/components/nmap_tracker/const.py
@@ -0,0 +1,16 @@
+"""The Nmap Tracker integration."""
+
+DOMAIN = "nmap_tracker"
+
+PLATFORMS = ["device_tracker"]
+
+NMAP_TRACKED_DEVICES = "nmap_tracked_devices"
+
+# Interval in minutes to exclude devices from a scan while they are home
+CONF_HOME_INTERVAL = "home_interval"
+CONF_OPTIONS = "scan_options"
+DEFAULT_OPTIONS = "-F --host-timeout 5s"
+
+TRACKER_SCAN_INTERVAL = 120
+
+DEFAULT_TRACK_NEW_DEVICES = True
diff --git a/homeassistant/components/nmap_tracker/strings.json b/homeassistant/components/nmap_tracker/strings.json
new file mode 100644
index 00000000000..ecb470a6f0d
--- /dev/null
+++ b/homeassistant/components/nmap_tracker/strings.json
@@ -0,0 +1,40 @@
+{
+ "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%]",
+ "exclude": "[%key:component::nmap_tracker::config::step::user::data::exclude%]",
+ "scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]",
+ "track_new_devices": "Track new devices",
+ "interval_seconds": "Scan interval"
+ }
+ }
+ },
+ "error": {
+ "invalid_hosts": "[%key:component::nmap_tracker::config::error::invalid_hosts%]"
+ }
+ },
+ "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).",
+ "data": {
+ "hosts": "Network addresses (comma seperated) to scan",
+ "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)",
+ "exclude": "Network addresses (comma seperated) to exclude from scanning",
+ "scan_options": "Raw configurable scan options for Nmap"
+ }
+ }
+ },
+ "error": {
+ "invalid_hosts": "Invalid Hosts"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nmap_tracker/translations/de.json b/homeassistant/components/nmap_tracker/translations/de.json
new file mode 100644
index 00000000000..67c7e8dbd8a
--- /dev/null
+++ b/homeassistant/components/nmap_tracker/translations/de.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Standort ist bereits konfiguriert"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "home_interval": "Mindestanzahl von Minuten zwischen den Scans aktiver Ger\u00e4te (Batterie schonen)",
+ "hosts": "Zu scannende Netzwerkadressen (kommagetrennt)",
+ "scan_options": "Raw konfigurierbare Scan-Optionen f\u00fcr Nmap"
+ },
+ "description": "Konfiguriere die Hosts, die von Nmap gescannt werden sollen. Netzwerkadresse und Ausschl\u00fcsse k\u00f6nnen IP-Adressen (192.168.1.1), IP-Netzwerke (192.168.0.0/24) oder IP-Bereiche (192.168.1.0-32) sein."
+ }
+ }
+ },
+ "title": "Nmap Tracker"
+}
\ No newline at end of file
diff --git a/homeassistant/components/nmap_tracker/translations/en.json b/homeassistant/components/nmap_tracker/translations/en.json
new file mode 100644
index 00000000000..6b83532a0e2
--- /dev/null
+++ b/homeassistant/components/nmap_tracker/translations/en.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Location is already configured"
+ },
+ "error": {
+ "invalid_hosts": "Invalid Hosts"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "exclude": "Network addresses (comma seperated) to exclude from scanning",
+ "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)",
+ "hosts": "Network addresses (comma seperated) 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)."
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "invalid_hosts": "Invalid Hosts"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "exclude": "Network addresses (comma seperated) to exclude from scanning",
+ "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)",
+ "hosts": "Network addresses (comma seperated) to scan",
+ "interval_seconds": "Scan interval",
+ "scan_options": "Raw configurable scan options for Nmap",
+ "track_new_devices": "Track new devices"
+ },
+ "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)."
+ }
+ }
+ },
+ "title": "Nmap Tracker"
+}
\ No newline at end of file
diff --git a/homeassistant/components/nmap_tracker/translations/et.json b/homeassistant/components/nmap_tracker/translations/et.json
new file mode 100644
index 00000000000..8b98dbc87cc
--- /dev/null
+++ b/homeassistant/components/nmap_tracker/translations/et.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Asukoht on juba m\u00e4\u00e4ratud"
+ },
+ "error": {
+ "invalid_hosts": "Sobimatud hostid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "exclude": "V\u00f5rguaadressid (komadega eraldatud), mis tuleb skaneerimisest v\u00e4lja j\u00e4tta",
+ "home_interval": "Minimaalne minutite arv aktiivsete seadmete skaneerimise vahel (aku s\u00e4ilitamine)",
+ "hosts": "Skaneeritavad v\u00f5rgu aadressid (komadega eraldatud)",
+ "scan_options": "Nmapi algseadistavad skaneerimisvalikud"
+ },
+ "description": "Konfigureeri hostid, mida Nmap skannib. V\u00f5rguaadress ja v\u00e4ljaj\u00e4etud v\u00f5ivad olla IP-aadressid (192.168.1.1), IP-v\u00f5rgud (192.168.0.0/24) v\u00f5i IP-vahemikud (192.168.1.0-32)."
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "invalid_hosts": "Vigased hostid"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "exclude": "V\u00e4listatud IP aadresside vahemik (komadega eraldatud list)",
+ "home_interval": "Minimaalne sk\u00e4nnimiste intervall minutites (eeldus on aku s\u00e4\u00e4stmine)",
+ "hosts": "V\u00f5rguaadresside vahemik (komaga eraldatud)",
+ "scan_options": "Vaikimisi Nmap sk\u00e4nnimise valikud"
+ },
+ "description": "Vali Nmap poolt sk\u00e4nnitavad hostid. Valikuks on IP aadressid (192.168.1.1), v\u00f5rgud (192.168.0.0/24) v\u00f5i IP vahemikud (192.168.1.0-32)."
+ }
+ }
+ },
+ "title": "Nmap asukoht"
+}
\ No newline at end of file
diff --git a/homeassistant/components/nmap_tracker/translations/nl.json b/homeassistant/components/nmap_tracker/translations/nl.json
new file mode 100644
index 00000000000..9f52a6b9add
--- /dev/null
+++ b/homeassistant/components/nmap_tracker/translations/nl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Locatie is al geconfigureerd."
+ },
+ "error": {
+ "invalid_hosts": "Ongeldige hosts"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "exclude": "Netwerkadressen (door komma's gescheiden) om uit te sluiten van scannen"
+ }
+ }
+ }
+ },
+ "title": "Nmap Tracker"
+}
\ No newline at end of file
diff --git a/homeassistant/components/nmap_tracker/translations/no.json b/homeassistant/components/nmap_tracker/translations/no.json
new file mode 100644
index 00000000000..487d15c910f
--- /dev/null
+++ b/homeassistant/components/nmap_tracker/translations/no.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Plasseringen er allerede konfigurert"
+ },
+ "error": {
+ "invalid_hosts": "Ugyldige verter"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "exclude": "Nettverksadresser (kommaseparert) for \u00e5 ekskludere fra skanning",
+ "home_interval": "Minimum antall minutter mellom skanninger av aktive enheter (lagre batteri)",
+ "hosts": "Nettverksadresser (kommaseparert) for \u00e5 skanne",
+ "scan_options": "R\u00e5 konfigurerbare skannealternativer for Nmap"
+ },
+ "description": "Konfigurer verter som skal skannes av Nmap. Nettverksadresse og ekskluderer kan v\u00e6re IP-adresser (192.168.1.1), IP-nettverk (192.168.0.0/24) eller IP-omr\u00e5der (192.168.1.0-32)."
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "invalid_hosts": "Ugyldige verter"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "exclude": "Nettverksadresser (kommaseparert) for \u00e5 ekskludere fra skanning",
+ "home_interval": "Minimum antall minutter mellom skanninger av aktive enheter (lagre batteri)",
+ "hosts": "Nettverksadresser (kommaseparert) for \u00e5 skanne",
+ "scan_options": "R\u00e5 konfigurerbare skannealternativer for Nmap"
+ },
+ "description": "Konfigurer verter som skal skannes av Nmap. Nettverksadresse og ekskluderer kan v\u00e6re IP-adresser (192.168.1.1), IP-nettverk (192.168.0.0/24) eller IP-omr\u00e5der (192.168.1.0-32)."
+ }
+ }
+ },
+ "title": "Sporing av Nmap"
+}
\ No newline at end of file
diff --git a/homeassistant/components/nmap_tracker/translations/ru.json b/homeassistant/components/nmap_tracker/translations/ru.json
new file mode 100644
index 00000000000..dc488f64ca6
--- /dev/null
+++ b/homeassistant/components/nmap_tracker/translations/ru.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ },
+ "error": {
+ "invalid_hosts": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "exclude": "\u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043b\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e)",
+ "home_interval": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043c\u0438\u043d\u0443\u0442 \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u044d\u043a\u043e\u043d\u043e\u043c\u0438\u044f \u0437\u0430\u0440\u044f\u0434\u0430 \u0431\u0430\u0442\u0430\u0440\u0435\u0438)",
+ "hosts": "\u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e)",
+ "scan_options": "\u041d\u0435\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u043b\u044f Nmap"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0445\u043e\u0441\u0442\u043e\u0432 \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f Nmap. \u0421\u0435\u0442\u0435\u0432\u044b\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0430 (192.168.1.1), IP-\u0441\u0435\u0442\u0438 (192.168.0.0/24) \u0438\u043b\u0438 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u044b IP-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 (192.168.1.0-32)."
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "invalid_hosts": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "exclude": "\u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043b\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e)",
+ "home_interval": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043c\u0438\u043d\u0443\u0442 \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u044d\u043a\u043e\u043d\u043e\u043c\u0438\u044f \u0437\u0430\u0440\u044f\u0434\u0430 \u0431\u0430\u0442\u0430\u0440\u0435\u0438)",
+ "hosts": "\u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e)",
+ "scan_options": "\u041d\u0435\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u043b\u044f Nmap"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0445\u043e\u0441\u0442\u043e\u0432 \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f Nmap. \u0421\u0435\u0442\u0435\u0432\u044b\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0430 (192.168.1.1), IP-\u0441\u0435\u0442\u0438 (192.168.0.0/24) \u0438\u043b\u0438 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u044b IP-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 (192.168.1.0-32)."
+ }
+ }
+ },
+ "title": "Nmap Tracker"
+}
\ No newline at end of file
diff --git a/homeassistant/components/nmap_tracker/translations/zh-Hans.json b/homeassistant/components/nmap_tracker/translations/zh-Hans.json
new file mode 100644
index 00000000000..e0ca0563b7a
--- /dev/null
+++ b/homeassistant/components/nmap_tracker/translations/zh-Hans.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "invalid_hosts": "\u4e3b\u673a\u65e0\u6548"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "exclude": "\u4ece\u626b\u63cf\u4e2d\u6392\u9664\u7684\u7f51\u7edc\u5730\u5740\uff08\u4ee5\u9017\u53f7\u5206\u9694\uff09",
+ "home_interval": "\u626b\u63cf\u8bbe\u5907\u7684\u6700\u5c0f\u95f4\u9694\u5206\u949f\u6570\uff08\u7528\u4e8e\u8282\u7701\u7535\u91cf\uff09",
+ "hosts": "\u8981\u626b\u63cf\u7684\u7f51\u7edc\u5730\u5740\uff08\u4ee5\u9017\u53f7\u5206\u9694\uff09",
+ "scan_options": "Nmap \u7684\u539f\u59cb\u53ef\u914d\u7f6e\u626b\u63cf\u9009\u9879"
+ },
+ "description": "\u914d\u7f6e\u901a\u8fc7 Nmap \u626b\u63cf\u7684\u4e3b\u673a\u3002\u7f51\u7edc\u5730\u5740\u548c\u6392\u9664\u9879\u53ef\u4ee5\u662f IP \u5730\u5740 (192.168.1.1)\u3001IP \u5730\u5740\u5757 (192.168.0.0/24) \u6216 IP \u8303\u56f4 (192.168.1.0-32)\u3002"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "invalid_hosts": "\u4e3b\u673a\u65e0\u6548"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "exclude": "\u4ece\u626b\u63cf\u4e2d\u6392\u9664\u7684\u7f51\u7edc\u5730\u5740\uff08\u4ee5\u9017\u53f7\u5206\u9694\uff09",
+ "home_interval": "\u626b\u63cf\u8bbe\u5907\u7684\u6700\u5c0f\u95f4\u9694\u5206\u949f\u6570\uff08\u7528\u4e8e\u8282\u7701\u7535\u91cf\uff09",
+ "hosts": "\u8981\u626b\u63cf\u7684\u7f51\u7edc\u5730\u5740\uff08\u4ee5\u9017\u53f7\u5206\u9694\uff09",
+ "scan_options": "Nmap \u7684\u539f\u59cb\u53ef\u914d\u7f6e\u626b\u63cf\u9009\u9879"
+ },
+ "description": "\u914d\u7f6e\u901a\u8fc7 Nmap \u626b\u63cf\u7684\u4e3b\u673a\u3002\u7f51\u7edc\u5730\u5740\u548c\u6392\u9664\u9879\u53ef\u4ee5\u662f IP \u5730\u5740 (192.168.1.1)\u3001IP \u5730\u5740\u5757 (192.168.0.0/24) \u6216 IP \u8303\u56f4 (192.168.1.0-32)\u3002"
+ }
+ }
+ },
+ "title": "Nmap \u8ddf\u8e2a\u5668"
+}
\ No newline at end of file
diff --git a/homeassistant/components/nmap_tracker/translations/zh-Hant.json b/homeassistant/components/nmap_tracker/translations/zh-Hant.json
new file mode 100644
index 00000000000..ce06358efc7
--- /dev/null
+++ b/homeassistant/components/nmap_tracker/translations/zh-Hant.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "invalid_hosts": "\u4e3b\u6a5f\u7aef\u7121\u6548"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "exclude": "\u6392\u9664\u6383\u63cf\u7684\u7db2\u8def\u4f4d\u5740\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09",
+ "home_interval": "\u6383\u63cf\u6d3b\u52d5\u88dd\u7f6e\u7684\u6700\u4f4e\u9593\u9694\u5206\u9418\uff08\u8003\u91cf\u7701\u96fb\uff09",
+ "hosts": "\u6240\u8981\u6383\u63cf\u7684\u7db2\u8def\u4f4d\u5740\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09",
+ "scan_options": "Nmap \u539f\u59cb\u8a2d\u5b9a\u6383\u63cf\u9078\u9805"
+ },
+ "description": "\u8a2d\u5b9a Nmap \u6383\u63cf\u4e3b\u6a5f\u3002\u7db2\u8def\u4f4d\u5740\u8207\u6392\u9664\u4f4d\u5740\u53ef\u4ee5\u662f IP \u4f4d\u5740\uff08192.168.1.1\uff09\u3001IP \u7db2\u8def\uff08192.168.0.0/24\uff09\u6216 IP \u7bc4\u570d\uff08192.168.1.0-32\uff09\u3002"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "invalid_hosts": "\u4e3b\u6a5f\u7aef\u7121\u6548"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "exclude": "\u6392\u9664\u6383\u63cf\u7684\u7db2\u8def\u4f4d\u5740\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09",
+ "home_interval": "\u6383\u63cf\u6d3b\u52d5\u88dd\u7f6e\u7684\u6700\u4f4e\u9593\u9694\u5206\u9418\uff08\u8003\u91cf\u7701\u96fb\uff09",
+ "hosts": "\u6240\u8981\u6383\u63cf\u7684\u7db2\u8def\u4f4d\u5740\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09",
+ "scan_options": "Nmap \u539f\u59cb\u8a2d\u5b9a\u6383\u63cf\u9078\u9805"
+ },
+ "description": "\u8a2d\u5b9a Nmap \u6383\u63cf\u4e3b\u6a5f\u3002\u7db2\u8def\u4f4d\u5740\u8207\u6392\u9664\u4f4d\u5740\u53ef\u4ee5\u662f IP \u4f4d\u5740\uff08192.168.1.1\uff09\u3001IP \u7db2\u8def\uff08192.168.0.0/24\uff09\u6216 IP \u7bc4\u570d\uff08192.168.1.0-32\uff09\u3002"
+ }
+ }
+ },
+ "title": "Nmap Tracker"
+}
\ No newline at end of file
diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py
index 58ad547eaec..26f7dbd2c8a 100644
--- a/homeassistant/components/nmbs/sensor.py
+++ b/homeassistant/components/nmbs/sensor.py
@@ -166,6 +166,8 @@ class NMBSLiveBoard(SensorEntity):
class NMBSSensor(SensorEntity):
"""Get the the total travel time for a given connection."""
+ _attr_unit_of_measurement = TIME_MINUTES
+
def __init__(
self, api_client, name, show_on_map, station_from, station_to, excl_vias
):
@@ -185,11 +187,6 @@ class NMBSSensor(SensorEntity):
"""Return the name of the sensor."""
return self._name
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return TIME_MINUTES
-
@property
def icon(self):
"""Return the sensor default icon or an alert icon if any delay."""
diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py
index 9efaac79562..2e9f5c77fbf 100644
--- a/homeassistant/components/no_ip/__init__.py
+++ b/homeassistant/components/no_ip/__init__.py
@@ -1,7 +1,7 @@
"""Integrate with NO-IP Dynamic DNS service."""
import asyncio
import base64
-from datetime import timedelta
+from datetime import datetime, timedelta
import logging
import aiohttp
@@ -10,8 +10,10 @@ import async_timeout
import voluptuous as vol
from homeassistant.const import CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -51,7 +53,7 @@ CONFIG_SCHEMA = vol.Schema(
)
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Initialize the NO-IP component."""
domain = config[DOMAIN].get(CONF_DOMAIN)
user = config[DOMAIN].get(CONF_USERNAME)
@@ -67,7 +69,7 @@ async def async_setup(hass, config):
if not result:
return False
- async def update_domain_interval(now):
+ async def update_domain_interval(now: datetime) -> None:
"""Update the NO-IP entry."""
await _update_no_ip(hass, session, domain, auth_str, timeout)
@@ -76,7 +78,13 @@ async def async_setup(hass, config):
return True
-async def _update_no_ip(hass, session, domain, auth_str, timeout):
+async def _update_no_ip(
+ hass: HomeAssistant,
+ session: aiohttp.ClientSession,
+ domain: str,
+ auth_str: bytes,
+ timeout: int,
+) -> bool:
"""Update NO-IP."""
url = UPDATE_URL
diff --git a/homeassistant/components/notion/translations/he.json b/homeassistant/components/notion/translations/he.json
index 3007c0e968c..1a397f894cf 100644
--- a/homeassistant/components/notion/translations/he.json
+++ b/homeassistant/components/notion/translations/he.json
@@ -1,9 +1,16 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
"step": {
"user": {
"data": {
- "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
}
diff --git a/homeassistant/components/nsw_fuel_station/__init__.py b/homeassistant/components/nsw_fuel_station/__init__.py
index 88ff1e779be..f64365461f4 100644
--- a/homeassistant/components/nsw_fuel_station/__init__.py
+++ b/homeassistant/components/nsw_fuel_station/__init__.py
@@ -1 +1,65 @@
"""The nsw_fuel_station component."""
+from __future__ import annotations
+
+from dataclasses import dataclass
+import datetime
+import logging
+
+from nsw_fuel import FuelCheckClient, FuelCheckError, Station
+
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import DATA_NSW_FUEL_STATION
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = "nsw_fuel_station"
+SCAN_INTERVAL = datetime.timedelta(hours=1)
+
+
+async def async_setup(hass, config):
+ """Set up the NSW Fuel Station platform."""
+ client = FuelCheckClient()
+
+ async def async_update_data():
+ return await hass.async_add_executor_job(fetch_station_price_data, client)
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name="sensor",
+ update_interval=SCAN_INTERVAL,
+ update_method=async_update_data,
+ )
+ hass.data[DATA_NSW_FUEL_STATION] = coordinator
+
+ await coordinator.async_refresh()
+
+ return True
+
+
+@dataclass
+class StationPriceData:
+ """Data structure for O(1) price and name lookups."""
+
+ stations: dict[int, Station]
+ prices: dict[tuple[int, str], float]
+
+
+def fetch_station_price_data(client: FuelCheckClient) -> StationPriceData | None:
+ """Fetch fuel price and station data."""
+ try:
+ raw_price_data = client.get_fuel_prices()
+ # Restructure prices and station details to be indexed by station code
+ # for O(1) lookup
+ return StationPriceData(
+ stations={s.code: s for s in raw_price_data.stations},
+ prices={
+ (p.station_code, p.fuel_type): p.price for p in raw_price_data.prices
+ },
+ )
+
+ except FuelCheckError as exc:
+ raise UpdateFailed(
+ f"Failed to fetch NSW Fuel station price data: {exc}"
+ ) from exc
diff --git a/homeassistant/components/nsw_fuel_station/const.py b/homeassistant/components/nsw_fuel_station/const.py
new file mode 100644
index 00000000000..885c8abf4a8
--- /dev/null
+++ b/homeassistant/components/nsw_fuel_station/const.py
@@ -0,0 +1,3 @@
+"""Constants for the NSW Fuel Station integration."""
+
+DATA_NSW_FUEL_STATION = "nsw_fuel_station"
diff --git a/homeassistant/components/nsw_fuel_station/manifest.json b/homeassistant/components/nsw_fuel_station/manifest.json
index 4dca09e77ea..dfc6ad62d90 100644
--- a/homeassistant/components/nsw_fuel_station/manifest.json
+++ b/homeassistant/components/nsw_fuel_station/manifest.json
@@ -2,7 +2,7 @@
"domain": "nsw_fuel_station",
"name": "NSW Fuel Station Price",
"documentation": "https://www.home-assistant.io/integrations/nsw_fuel_station",
- "requirements": ["nsw-fuel-api-client==1.0.10"],
+ "requirements": ["nsw-fuel-api-client==1.1.0"],
"codeowners": ["@nickw444"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py
index 9522d6c430f..52536e69027 100644
--- a/homeassistant/components/nsw_fuel_station/sensor.py
+++ b/homeassistant/components/nsw_fuel_station/sensor.py
@@ -1,16 +1,21 @@
"""Sensor platform to display the current fuel prices at a NSW fuel station."""
from __future__ import annotations
-import datetime
import logging
-from nsw_fuel import FuelCheckClient, FuelCheckError
import voluptuous as vol
+from homeassistant.components.nsw_fuel_station import (
+ DATA_NSW_FUEL_STATION,
+ StationPriceData,
+)
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_CENT, VOLUME_LITERS
import homeassistant.helpers.config_validation as cv
-from homeassistant.util import Throttle
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
_LOGGER = logging.getLogger(__name__)
@@ -35,7 +40,6 @@ CONF_ALLOWED_FUEL_TYPES = [
CONF_DEFAULT_FUEL_TYPES = ["E10", "U91"]
ATTRIBUTION = "Data provided by NSW Government FuelCheck"
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_STATION_ID): cv.positive_int,
@@ -45,11 +49,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
}
)
-MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(hours=1)
-
-NOTIFICATION_ID = "nsw_fuel_station_notification"
-NOTIFICATION_TITLE = "NSW Fuel Station Sensor Setup"
-
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the NSW Fuel Station sensor."""
@@ -57,122 +56,63 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
station_id = config[CONF_STATION_ID]
fuel_types = config[CONF_FUEL_TYPES]
- client = FuelCheckClient()
- station_data = StationPriceData(client, station_id)
- station_data.update()
+ coordinator = hass.data[DATA_NSW_FUEL_STATION]
- if station_data.error is not None:
- message = ("Error: {}. Check the logs for additional information.").format(
- station_data.error
- )
-
- hass.components.persistent_notification.create(
- message, title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID
- )
+ if coordinator.data is None:
+ _LOGGER.error("Initial fuel station price data not available")
return
- available_fuel_types = station_data.get_available_fuel_types()
+ entities = []
+ for fuel_type in fuel_types:
+ if coordinator.data.prices.get((station_id, fuel_type)) is None:
+ _LOGGER.error(
+ "Fuel station price data not available for station %d and fuel type %s",
+ station_id,
+ fuel_type,
+ )
+ continue
- add_entities(
- [
- StationPriceSensor(station_data, fuel_type)
- for fuel_type in fuel_types
- if fuel_type in available_fuel_types
- ]
- )
+ entities.append(StationPriceSensor(coordinator, station_id, fuel_type))
+
+ add_entities(entities)
-class StationPriceData:
- """An object to store and fetch the latest data for a given station."""
-
- def __init__(self, client, station_id: int) -> None:
- """Initialize the sensor."""
- self.station_id = station_id
- self._client = client
- self._data = None
- self._reference_data = None
- self.error = None
- self._station_name = None
-
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self):
- """Update the internal data using the API client."""
-
- if self._reference_data is None:
- try:
- self._reference_data = self._client.get_reference_data()
- except FuelCheckError as exc:
- self.error = str(exc)
- _LOGGER.error(
- "Failed to fetch NSW Fuel station reference data. %s", exc
- )
- return
-
- try:
- self._data = self._client.get_fuel_prices_for_station(self.station_id)
- except FuelCheckError as exc:
- self.error = str(exc)
- _LOGGER.error("Failed to fetch NSW Fuel station price data. %s", exc)
-
- def for_fuel_type(self, fuel_type: str):
- """Return the price of the given fuel type."""
- if self._data is None:
- return None
- return next(
- (price for price in self._data if price.fuel_type == fuel_type), None
- )
-
- def get_available_fuel_types(self):
- """Return the available fuel types for the station."""
- return [price.fuel_type for price in self._data]
-
- def get_station_name(self) -> str:
- """Return the name of the station."""
- if self._station_name is None:
- name = None
- if self._reference_data is not None:
- name = next(
- (
- station.name
- for station in self._reference_data.stations
- if station.code == self.station_id
- ),
- None,
- )
-
- self._station_name = name or f"station {self.station_id}"
-
- return self._station_name
-
-
-class StationPriceSensor(SensorEntity):
+class StationPriceSensor(CoordinatorEntity, SensorEntity):
"""Implementation of a sensor that reports the fuel price for a station."""
- def __init__(self, station_data: StationPriceData, fuel_type: str) -> None:
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator[StationPriceData],
+ station_id: int,
+ fuel_type: str,
+ ) -> None:
"""Initialize the sensor."""
- self._station_data = station_data
+ super().__init__(coordinator)
+
+ self._station_id = station_id
self._fuel_type = fuel_type
@property
def name(self) -> str:
"""Return the name of the sensor."""
- return f"{self._station_data.get_station_name()} {self._fuel_type}"
+ station_name = self._get_station_name()
+ return f"{station_name} {self._fuel_type}"
@property
def state(self) -> float | None:
"""Return the state of the sensor."""
- price_info = self._station_data.for_fuel_type(self._fuel_type)
- if price_info:
- return price_info.price
+ if self.coordinator.data is None:
+ return None
- return None
+ prices = self.coordinator.data.prices
+ return prices.get((self._station_id, self._fuel_type))
@property
def extra_state_attributes(self) -> dict:
"""Return the state attributes of the device."""
return {
- ATTR_STATION_ID: self._station_data.station_id,
- ATTR_STATION_NAME: self._station_data.get_station_name(),
+ ATTR_STATION_ID: self._station_id,
+ ATTR_STATION_NAME: self._get_station_name(),
ATTR_ATTRIBUTION: ATTRIBUTION,
}
@@ -181,6 +121,18 @@ class StationPriceSensor(SensorEntity):
"""Return the units of measurement."""
return f"{CURRENCY_CENT}/{VOLUME_LITERS}"
- def update(self):
- """Update current conditions."""
- self._station_data.update()
+ def _get_station_name(self):
+ default_name = f"station {self._station_id}"
+ if self.coordinator.data is None:
+ return default_name
+
+ station = self.coordinator.data.stations.get(self._station_id)
+ if station is None:
+ return default_name
+
+ return station.name
+
+ @property
+ def unique_id(self) -> str | None:
+ """Return a unique ID."""
+ return f"{self._station_id}_{self._fuel_type}"
diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json
index debc255ec7f..ce75e72f5de 100644
--- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json
+++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json
@@ -2,7 +2,7 @@
"domain": "nsw_rural_fire_service_feed",
"name": "NSW Rural Fire Service Incidents",
"documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed",
- "requirements": ["aio_geojson_nsw_rfs_incidents==0.3"],
+ "requirements": ["aio_geojson_nsw_rfs_incidents==0.4"],
"codeowners": ["@exxamalte"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py
index db50a9a70d9..08cacd2bf76 100644
--- a/homeassistant/components/nuheat/__init__.py
+++ b/homeassistant/components/nuheat/__init__.py
@@ -30,7 +30,7 @@ def _get_thermostat(api, serial_number):
return api.get_thermostat(serial_number)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up NuHeat from a config entry."""
conf = entry.data
diff --git a/homeassistant/components/nuheat/translations/he.json b/homeassistant/components/nuheat/translations/he.json
index ac90b3264ea..c479d8488f2 100644
--- a/homeassistant/components/nuheat/translations/he.json
+++ b/homeassistant/components/nuheat/translations/he.json
@@ -1,5 +1,13 @@
{
"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",
+ "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": {
diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py
index 37641dbf15a..3b79eef324f 100644
--- a/homeassistant/components/nuki/binary_sensor.py
+++ b/homeassistant/components/nuki/binary_sensor.py
@@ -29,6 +29,8 @@ async def async_setup_entry(hass, entry, async_add_entities):
class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity):
"""Representation of a Nuki Lock Doorsensor."""
+ _attr_device_class = DEVICE_CLASS_DOOR
+
@property
def name(self):
"""Return the name of the lock."""
@@ -66,8 +68,3 @@ class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity):
def is_on(self):
"""Return true if the door is open."""
return self.door_sensor_state == STATE_DOORSENSOR_OPENED
-
- @property
- def device_class(self):
- """Return the class of this device, from component DEVICE_CLASSES."""
- return DEVICE_CLASS_DOOR
diff --git a/homeassistant/components/nuki/const.py b/homeassistant/components/nuki/const.py
index da12a3a074d..680454c3edc 100644
--- a/homeassistant/components/nuki/const.py
+++ b/homeassistant/components/nuki/const.py
@@ -4,6 +4,7 @@ DOMAIN = "nuki"
# Attributes
ATTR_BATTERY_CRITICAL = "battery_critical"
ATTR_NUKI_ID = "nuki_id"
+ATTR_ENABLE = "enable"
ATTR_UNLATCH = "unlatch"
# Data
diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py
index 48e72b88530..9cb1bd01524 100644
--- a/homeassistant/components/nuki/lock.py
+++ b/homeassistant/components/nuki/lock.py
@@ -12,6 +12,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform
from . import NukiEntity
from .const import (
ATTR_BATTERY_CRITICAL,
+ ATTR_ENABLE,
ATTR_NUKI_ID,
ATTR_UNLATCH,
DATA_COORDINATOR,
@@ -58,6 +59,11 @@ async def async_setup_entry(hass, entry, async_add_entities):
vol.Optional(ATTR_UNLATCH, default=False): cv.boolean,
},
"lock_n_go",
+ "set_continuous_mode",
+ {
+ vol.Required(ATTR_ENABLE): cv.boolean,
+ },
+ "set_continuous_mode",
)
@@ -165,3 +171,15 @@ class NukiOpenerEntity(NukiDeviceEntity):
def lock_n_go(self, unlatch):
"""Stub service."""
+
+ def set_continuous_mode(self, enable):
+ """Continuous Mode.
+
+ This feature will cause the door to automatically open when anyone
+ rings the bell. This is similar to ring-to-open, except that it does
+ not automatically deactivate
+ """
+ if enable:
+ self._nuki_device.activate_continuous_mode()
+ else:
+ self._nuki_device.deactivate_continuous_mode()
diff --git a/homeassistant/components/nuki/services.yaml b/homeassistant/components/nuki/services.yaml
index d923885efc6..c43f081dbf7 100644
--- a/homeassistant/components/nuki/services.yaml
+++ b/homeassistant/components/nuki/services.yaml
@@ -12,3 +12,17 @@ lock_n_go:
default: false
selector:
boolean:
+set_continuous_mode:
+ name: Set Continuous Mode
+ description: "Enable or disable Continuous Mode on Nuki Opener"
+ target:
+ entity:
+ integration: nuki
+ domain: lock
+ fields:
+ enable:
+ name: Enable
+ description: Whether to enable or disable the feature
+ default: false
+ selector:
+ boolean:
diff --git a/homeassistant/components/nuki/translations/de.json b/homeassistant/components/nuki/translations/de.json
index ae1322d7641..2631c7b0588 100644
--- a/homeassistant/components/nuki/translations/de.json
+++ b/homeassistant/components/nuki/translations/de.json
@@ -13,6 +13,7 @@
"data": {
"token": "Zugangstoken"
},
+ "description": "Die Nuki-Integration muss sich bei deiner Bridge neu authentifizieren.",
"title": "Integration erneut authentifizieren"
},
"user": {
diff --git a/homeassistant/components/nuki/translations/he.json b/homeassistant/components/nuki/translations/he.json
new file mode 100644
index 00000000000..971fe867c28
--- /dev/null
+++ b/homeassistant/components/nuki/translations/he.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4"
+ },
+ "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1"
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py
index 046895ac29c..cbfdea7fa11 100644
--- a/homeassistant/components/number/__init__.py
+++ b/homeassistant/components/number/__init__.py
@@ -1,10 +1,9 @@
"""Component to allow numeric input for platforms."""
from __future__ import annotations
-from abc import abstractmethod
from datetime import timedelta
import logging
-from typing import Any
+from typing import Any, final
import voluptuous as vol
@@ -57,17 +56,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry) # type: ignore
+ 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."""
- return await hass.data[DOMAIN].async_unload_entry(entry) # type: ignore
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class NumberEntity(Entity):
"""Representation of a Number entity."""
+ _attr_max_value: float = DEFAULT_MAX_VALUE
+ _attr_min_value: float = DEFAULT_MIN_VALUE
+ _attr_state: None = None
+ _attr_step: float
+ _attr_value: float
+
@property
def capability_attributes(self) -> dict[str, Any]:
"""Return capability attributes."""
@@ -80,16 +87,18 @@ class NumberEntity(Entity):
@property
def min_value(self) -> float:
"""Return the minimum value."""
- return DEFAULT_MIN_VALUE
+ return self._attr_min_value
@property
def max_value(self) -> float:
"""Return the maximum value."""
- return DEFAULT_MAX_VALUE
+ return self._attr_max_value
@property
def step(self) -> float:
"""Return the increment/decrement step."""
+ if hasattr(self, "_attr_step"):
+ return self._attr_step
step = DEFAULT_STEP
value_range = abs(self.max_value - self.min_value)
if value_range != 0:
@@ -98,14 +107,15 @@ class NumberEntity(Entity):
return step
@property
- def state(self) -> float:
+ @final
+ def state(self) -> float | None:
"""Return the entity state."""
return self.value
@property
- @abstractmethod
- def value(self) -> float:
+ def value(self) -> float | None:
"""Return the entity value to represent the entity state."""
+ return self._attr_value
def set_value(self, value: float) -> None:
"""Set new value."""
diff --git a/homeassistant/components/number/translations/he.json b/homeassistant/components/number/translations/he.json
new file mode 100644
index 00000000000..b71257ef066
--- /dev/null
+++ b/homeassistant/components/number/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "\u05d4\u05d2\u05d3\u05e8 \u05e2\u05e8\u05da \u05e2\u05d1\u05d5\u05e8 {entity_name}"
+ }
+ },
+ "title": "\u05de\u05e1\u05e4\u05e8"
+}
\ No newline at end of file
diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py
index 77458b2cfb7..5b5389f0270 100644
--- a/homeassistant/components/nut/__init__.py
+++ b/homeassistant/components/nut/__init__.py
@@ -36,7 +36,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Network UPS Tools (NUT) from a config entry."""
config = entry.data
diff --git a/homeassistant/components/nut/translations/he.json b/homeassistant/components/nut/translations/he.json
index ac90b3264ea..77c12630351 100644
--- a/homeassistant/components/nut/translations/he.json
+++ b/homeassistant/components/nut/translations/he.json
@@ -1,12 +1,27 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
"user": {
"data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
}
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py
index 47465739250..0e00c848970 100644
--- a/homeassistant/components/nws/__init__.py
+++ b/homeassistant/components/nws/__init__.py
@@ -93,7 +93,7 @@ class NwsDataUpdateCoordinator(DataUpdateCoordinator):
)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a National Weather Service entry."""
latitude = entry.data[CONF_LATITUDE]
longitude = entry.data[CONF_LONGITUDE]
diff --git a/homeassistant/components/nws/translations/de.json b/homeassistant/components/nws/translations/de.json
index 3d409bf885b..b6899e34789 100644
--- a/homeassistant/components/nws/translations/de.json
+++ b/homeassistant/components/nws/translations/de.json
@@ -15,7 +15,7 @@
"longitude": "L\u00e4ngengrad",
"station": "METAR Stationscode"
},
- "description": "Wenn kein METAR-Stationscode angegeben ist, werden L\u00e4ngen- und Breitengrad verwendet, um die n\u00e4chstgelegene Station zu finden.",
+ "description": "Wenn kein METAR-Stationscode angegeben wird, werden der Breiten- und L\u00e4ngengrad verwendet, um die n\u00e4chstgelegene Station zu finden. Im Moment kann ein API-Schl\u00fcssel alles sein. Es wird empfohlen, eine g\u00fcltige E-Mail-Adresse zu verwenden.",
"title": "Stellen Sie eine Verbindung zum Nationalen Wetterdienst her"
}
}
diff --git a/homeassistant/components/nws/translations/he.json b/homeassistant/components/nws/translations/he.json
index 4c49313d977..3ba18c5b4ab 100644
--- a/homeassistant/components/nws/translations/he.json
+++ b/homeassistant/components/nws/translations/he.json
@@ -1,8 +1,17 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
"user": {
"data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
"longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
}
}
diff --git a/homeassistant/components/nzbget/translations/de.json b/homeassistant/components/nzbget/translations/de.json
index 529eff3d9a2..74d073ce292 100644
--- a/homeassistant/components/nzbget/translations/de.json
+++ b/homeassistant/components/nzbget/translations/de.json
@@ -7,7 +7,7 @@
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
},
- "flow_title": "NZBGet: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/nzbget/translations/he.json b/homeassistant/components/nzbget/translations/he.json
new file mode 100644
index 00000000000..f1a32be9a5c
--- /dev/null
+++ b/homeassistant/components/nzbget/translations/he.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "name": "\u05e9\u05dd",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nzbget/translations/hu.json b/homeassistant/components/nzbget/translations/hu.json
index 9eee6cc3be6..829fa03fe8e 100644
--- a/homeassistant/components/nzbget/translations/hu.json
+++ b/homeassistant/components/nzbget/translations/hu.json
@@ -7,7 +7,7 @@
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
- "flow_title": "NZBGet: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py
index 8d2071dee7c..556c100033b 100644
--- a/homeassistant/components/omnilogic/__init__.py
+++ b/homeassistant/components/omnilogic/__init__.py
@@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor", "switch"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Omnilogic from a config entry."""
conf = entry.data
diff --git a/homeassistant/components/omnilogic/translations/he.json b/homeassistant/components/omnilogic/translations/he.json
new file mode 100644
index 00000000000..b38e5b12c8b
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/he.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea."
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "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": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onboarding/translations/he.json b/homeassistant/components/onboarding/translations/he.json
new file mode 100644
index 00000000000..683b005376f
--- /dev/null
+++ b/homeassistant/components/onboarding/translations/he.json
@@ -0,0 +1,7 @@
+{
+ "area": {
+ "bedroom": "\u05d7\u05d3\u05e8 \u05e9\u05d9\u05e0\u05d4",
+ "kitchen": "\u05de\u05d8\u05d1\u05d7",
+ "living_room": "\u05e1\u05dc\u05d5\u05df"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/he.json b/homeassistant/components/ondilo_ico/translations/he.json
new file mode 100644
index 00000000000..be83e5f2fed
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/he.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3."
+ },
+ "create_entry": {
+ "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py
index a27e1a49ab1..5ba813ce368 100644
--- a/homeassistant/components/onewire/__init__.py
+++ b/homeassistant/components/onewire/__init__.py
@@ -13,17 +13,17 @@ from .onewirehub import CannotConnect, OneWireHub
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a 1-Wire proxy for a config entry."""
hass.data.setdefault(DOMAIN, {})
onewirehub = OneWireHub(hass)
try:
- await onewirehub.initialize(config_entry)
+ await onewirehub.initialize(entry)
except CannotConnect as exc:
raise ConfigEntryNotReady() from exc
- hass.data[DOMAIN][config_entry.entry_id] = onewirehub
+ hass.data[DOMAIN][entry.entry_id] = onewirehub
async def cleanup_registry() -> None:
# Get registries
@@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
registry_devices = [
entry.id
for entry in dr.async_entries_for_config_entry(
- device_registry, config_entry.entry_id
+ device_registry, entry.entry_id
)
]
# Remove devices that don't belong to any entity
@@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
# wait until all required platforms are ready
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
for platform in PLATFORMS
]
)
diff --git a/homeassistant/components/onewire/translations/he.json b/homeassistant/components/onewire/translations/he.json
new file mode 100644
index 00000000000..d83d1f76175
--- /dev/null
+++ b/homeassistant/components/onewire/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": {
+ "owserver": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py
index f90ccb16760..5c44cdf1750 100644
--- a/homeassistant/components/onvif/__init__.py
+++ b/homeassistant/components/onvif/__init__.py
@@ -59,7 +59,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up ONVIF from a config entry."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py
index 1fa904e67e4..b4193f0def7 100644
--- a/homeassistant/components/onvif/config_flow.py
+++ b/homeassistant/components/onvif/config_flow.py
@@ -91,10 +91,15 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input=None):
"""Handle user flow."""
- if user_input is not None:
- return await self.async_step_device()
+ if user_input:
+ if user_input["auto"]:
+ return await self.async_step_device()
+ return await self.async_step_configure()
- return self.async_show_form(step_id="user")
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema({vol.Required("auto", default=True): bool}),
+ )
async def async_step_device(self, user_input=None):
"""Handle WS-Discovery.
@@ -105,7 +110,7 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input:
if CONF_MANUAL_INPUT == user_input[CONF_HOST]:
- return await self.async_step_manual_input()
+ return await self.async_step_configure()
for device in self.devices:
name = f"{device[CONF_NAME]} ({device[CONF_HOST]})"
@@ -116,7 +121,7 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONF_HOST: device[CONF_HOST],
CONF_PORT: device[CONF_PORT],
}
- return await self.async_step_auth()
+ return await self.async_step_configure()
discovery = await async_discovery(self.hass)
for device in discovery:
@@ -142,50 +147,41 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema({vol.Optional(CONF_HOST): vol.In(names)}),
)
- return await self.async_step_manual_input()
+ return await self.async_step_configure()
- async def async_step_manual_input(self, user_input=None):
- """Manual configuration."""
+ async def async_step_configure(self, user_input=None):
+ """Device configuration."""
+ errors = {}
if user_input:
self.onvif_config = user_input
- return await self.async_step_auth()
+ try:
+ return await self.async_setup_profiles()
+ except Fault:
+ errors["base"] = "cannot_connect"
- return self.async_show_form(
- step_id="manual_input",
- data_schema=vol.Schema(
- {
- vol.Required(CONF_NAME): str,
- vol.Required(CONF_HOST): str,
- vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
- }
- ),
- )
-
- async def async_step_auth(self, user_input=None):
- """Username and Password configuration for ONVIF device."""
- if user_input:
- self.onvif_config[CONF_USERNAME] = user_input[CONF_USERNAME]
- self.onvif_config[CONF_PASSWORD] = user_input[CONF_PASSWORD]
- return await self.async_step_profiles()
+ def conf(name, default=None):
+ return self.onvif_config.get(name, default)
# Username and Password are optional and default empty
# due to some cameras not allowing you to change ONVIF user settings.
# See https://github.com/home-assistant/core/issues/39182
# and https://github.com/home-assistant/core/issues/35904
return self.async_show_form(
- step_id="auth",
+ step_id="configure",
data_schema=vol.Schema(
{
- vol.Optional(CONF_USERNAME, default=""): str,
- vol.Optional(CONF_PASSWORD, default=""): str,
+ vol.Required(CONF_NAME, default=conf(CONF_NAME)): str,
+ vol.Required(CONF_HOST, default=conf(CONF_HOST)): str,
+ vol.Required(CONF_PORT, default=conf(CONF_PORT, DEFAULT_PORT)): int,
+ vol.Optional(CONF_USERNAME, default=conf(CONF_USERNAME, "")): str,
+ vol.Optional(CONF_PASSWORD, default=conf(CONF_PASSWORD, "")): str,
}
),
+ errors=errors,
)
- async def async_step_profiles(self, user_input=None):
+ async def async_setup_profiles(self):
"""Fetch ONVIF device profiles."""
- errors = {}
-
LOGGER.debug(
"Fetching profiles from ONVIF device %s", pformat(self.onvif_config)
)
@@ -262,18 +258,12 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
)
return self.async_abort(reason="onvif_error")
- except Fault:
- errors["base"] = "cannot_connect"
-
finally:
await device.close()
- return self.async_show_form(step_id="auth", errors=errors)
-
async def async_step_import(self, user_input):
"""Handle import."""
- self.onvif_config = user_input
- return await self.async_step_profiles()
+ return await self.async_step_configure(user_input)
class OnvifOptionsFlowHandler(config_entries.OptionsFlow):
diff --git a/homeassistant/components/onvif/strings.json b/homeassistant/components/onvif/strings.json
index dac8ef8647d..4cf1bd4bad0 100644
--- a/homeassistant/components/onvif/strings.json
+++ b/homeassistant/components/onvif/strings.json
@@ -12,6 +12,9 @@
},
"step": {
"user": {
+ "data": {
+ "auto": "Search automatically"
+ },
"title": "ONVIF device setup",
"description": "By clicking submit, we will search your network for ONVIF devices that support Profile S.\n\nSome manufacturers have started to disable ONVIF by default. Please ensure ONVIF is enabled in your camera's configuration."
},
@@ -21,20 +24,15 @@
},
"title": "Select ONVIF device"
},
- "manual_input": {
+ "configure": {
"data": {
"name": "[%key:common::config_flow::data::name%]",
"host": "[%key:common::config_flow::data::host%]",
- "port": "[%key:common::config_flow::data::port%]"
- },
- "title": "Configure ONVIF device"
- },
- "auth": {
- "title": "Configure authentication",
- "data": {
+ "port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
- }
+ },
+ "title": "Configure ONVIF device"
},
"configure_profile": {
"description": "Create camera entity for {profile} at {resolution} resolution?",
diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json
index 5289f6479cc..109e6256791 100644
--- a/homeassistant/components/onvif/translations/de.json
+++ b/homeassistant/components/onvif/translations/de.json
@@ -18,6 +18,16 @@
},
"title": "Konfigurieren Sie die Authentifizierung"
},
+ "configure": {
+ "data": {
+ "host": "Host",
+ "name": "Name",
+ "password": "Passwort",
+ "port": "Port",
+ "username": "Benutzername"
+ },
+ "title": "Konfigurieren Sie das ONVIF-Ger\u00e4t"
+ },
"configure_profile": {
"data": {
"include": "Kameraentit\u00e4t erstellen"
@@ -40,6 +50,9 @@
"title": "Konfigurieren Sie das ONVIF-Ger\u00e4t"
},
"user": {
+ "data": {
+ "auto": "Automatisch suchen"
+ },
"description": "Wenn Sie auf Senden klicken, durchsuchen wir Ihr Netzwerk nach ONVIF-Ger\u00e4ten, die Profil S unterst\u00fctzen. \n\nEinige Hersteller haben begonnen, ONVIF standardm\u00e4\u00dfig zu deaktivieren. Stellen Sie sicher, dass ONVIF in der Konfiguration Ihrer Kamera aktiviert ist.",
"title": "ONVIF-Ger\u00e4tekonfiguration"
}
diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json
index f52b96fbdc5..c922fc18482 100644
--- a/homeassistant/components/onvif/translations/en.json
+++ b/homeassistant/components/onvif/translations/en.json
@@ -18,6 +18,16 @@
},
"title": "Configure authentication"
},
+ "configure": {
+ "data": {
+ "host": "Host",
+ "name": "Name",
+ "password": "Password",
+ "port": "Port",
+ "username": "Username"
+ },
+ "title": "Configure ONVIF device"
+ },
"configure_profile": {
"data": {
"include": "Create camera entity"
@@ -40,6 +50,9 @@
"title": "Configure ONVIF device"
},
"user": {
+ "data": {
+ "auto": "Search automatically"
+ },
"description": "By clicking submit, we will search your network for ONVIF devices that support Profile S.\n\nSome manufacturers have started to disable ONVIF by default. Please ensure ONVIF is enabled in your camera's configuration.",
"title": "ONVIF device setup"
}
diff --git a/homeassistant/components/onvif/translations/et.json b/homeassistant/components/onvif/translations/et.json
index 9ba14ffaa52..61eefa84d12 100644
--- a/homeassistant/components/onvif/translations/et.json
+++ b/homeassistant/components/onvif/translations/et.json
@@ -18,6 +18,16 @@
},
"title": "Autentimise seadistamine"
},
+ "configure": {
+ "data": {
+ "host": "Host",
+ "name": "Nimi",
+ "password": "Salas\u00f5na",
+ "port": "Port",
+ "username": "Kasutajanimi"
+ },
+ "title": "Seadista ONVIF-seade"
+ },
"configure_profile": {
"data": {
"include": "Loo kaamera olem"
@@ -40,6 +50,9 @@
"title": "H\u00e4\u00e4lesta ONVIF-seade"
},
"user": {
+ "data": {
+ "auto": "Otsi automaatselt"
+ },
"description": "Kl\u00f5psates nuppu Esita, otsime v\u00f5rgust ONVIF-seadmeid, mis toetavad Profile S'i.\n\n M\u00f5ned tootjad on ONVIF-i vaikimisi keelanud. Veendu, et ONVIF on kaamera seadistustes lubatud.",
"title": "ONVIF-seadme seadistamine"
}
diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json
index ecfa1afaab2..ec9da5b556e 100644
--- a/homeassistant/components/onvif/translations/he.json
+++ b/homeassistant/components/onvif/translations/he.json
@@ -1,5 +1,14 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea",
+ "no_h264": "\u05dc\u05d0 \u05d4\u05d9\u05d5 \u05d6\u05e8\u05de\u05d9 H264 \u05d6\u05de\u05d9\u05e0\u05d9\u05dd. \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc \u05d1\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05dc\u05da.",
+ "no_mac": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e7\u05d1\u05d5\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05de\u05d6\u05d4\u05d4 \u05d9\u05d9\u05d7\u05d5\u05d3\u05d9 \u05e2\u05d1\u05d5\u05e8 \u05d4\u05ea\u05e7\u05df ONVIF."
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
"step": {
"auth": {
"data": {
@@ -10,13 +19,37 @@
"configure_profile": {
"data": {
"include": "\u05e6\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05de\u05e6\u05dc\u05de\u05d4"
- }
+ },
+ "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05e4\u05e8\u05d5\u05e4\u05d9\u05dc\u05d9\u05dd"
+ },
+ "device": {
+ "data": {
+ "host": "\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df ONVIF \u05e9\u05d4\u05ea\u05d2\u05dc\u05d4"
+ },
+ "title": "\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df ONVIF"
},
"manual_input": {
"data": {
- "host": "Host",
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "name": "\u05e9\u05dd",
"port": "\u05e4\u05d5\u05e8\u05d8"
- }
+ },
+ "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d4\u05ea\u05e7\u05df ONVIF"
+ },
+ "user": {
+ "description": "\u05d1\u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05e9\u05dc\u05d7, \u05e0\u05d7\u05e4\u05e9 \u05d1\u05e8\u05e9\u05ea \u05e9\u05dc\u05da \u05de\u05db\u05e9\u05d9\u05e8\u05d9 ONVIF \u05d4\u05ea\u05d5\u05de\u05db\u05d9\u05dd \u05d1\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc S.\n\n\u05d9\u05e6\u05e8\u05e0\u05d9\u05dd \u05de\u05e1\u05d5\u05d9\u05de\u05d9\u05dd \u05d4\u05d7\u05dc\u05d5 \u05dc\u05d4\u05e9\u05d1\u05d9\u05ea \u05d0\u05ea ONVIF \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05e0\u05d0 \u05d5\u05d3\u05d0 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea ONVIF \u05d6\u05de\u05d9\u05e0\u05d4 \u05d1\u05de\u05e6\u05dc\u05de\u05d4 \u05e9\u05dc\u05da.",
+ "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d4\u05ea\u05e7\u05df ONVIF"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "onvif_devices": {
+ "data": {
+ "extra_arguments": "\u05d0\u05e8\u05d2\u05d5\u05de\u05e0\u05d8\u05d9\u05dd \u05e0\u05d5\u05e1\u05e4\u05d9\u05dd \u05e9\u05dc FFMPEG",
+ "rtsp_transport": "\u05de\u05e0\u05d2\u05e0\u05d5\u05df \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 RTSP"
+ },
+ "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05d4\u05ea\u05e7\u05df ONVIF"
}
}
}
diff --git a/homeassistant/components/onvif/translations/nl.json b/homeassistant/components/onvif/translations/nl.json
index e1fdac8256e..c48d2764672 100644
--- a/homeassistant/components/onvif/translations/nl.json
+++ b/homeassistant/components/onvif/translations/nl.json
@@ -18,6 +18,16 @@
},
"title": "Configureer authenticatie"
},
+ "configure": {
+ "data": {
+ "host": "Host",
+ "name": "Naam",
+ "password": "Wachtwoord",
+ "port": "Poort",
+ "username": "Gebruikersnaam"
+ },
+ "title": "Configureer ONVIF-apparaat"
+ },
"configure_profile": {
"data": {
"include": "Cameraentiteit maken"
@@ -40,6 +50,9 @@
"title": "Configureer ONVIF-apparaat"
},
"user": {
+ "data": {
+ "auto": "Automatisch zoeken"
+ },
"description": "Door op verzenden te klikken, zoeken we in uw netwerk naar ONVIF-apparaten die Profiel S ondersteunen. \n\nSommige fabrikanten zijn begonnen ONVIF standaard uit te schakelen. Zorg ervoor dat ONVIF is ingeschakeld in de configuratie van uw camera.",
"title": "ONVIF-apparaat instellen"
}
diff --git a/homeassistant/components/onvif/translations/no.json b/homeassistant/components/onvif/translations/no.json
index e30f4e4e909..323f9aba5fe 100644
--- a/homeassistant/components/onvif/translations/no.json
+++ b/homeassistant/components/onvif/translations/no.json
@@ -18,6 +18,16 @@
},
"title": "Konfigurere godkjenning"
},
+ "configure": {
+ "data": {
+ "host": "Vert",
+ "name": "Navn",
+ "password": "Passord",
+ "port": "Port",
+ "username": "Brukernavn"
+ },
+ "title": "Konfigurere ONVIF-enhet"
+ },
"configure_profile": {
"data": {
"include": "Lag kameraentitet"
@@ -40,6 +50,9 @@
"title": "Konfigurere ONVIF-enhet"
},
"user": {
+ "data": {
+ "auto": "S\u00f8k automatisk"
+ },
"description": "Ved \u00e5 klikke send inn, vil vi s\u00f8ke nettverket etter ONVIF-enheter som st\u00f8tter Profil S.\n\nNoen produsenter har begynt \u00e5 deaktivere ONVIF som standard. Vennligst kontroller at ONVIF er aktivert i kameraets konfigurasjon.",
"title": "ONVIF enhetsoppsett"
}
diff --git a/homeassistant/components/onvif/translations/ru.json b/homeassistant/components/onvif/translations/ru.json
index 6b486bb62e2..55d6549645d 100644
--- a/homeassistant/components/onvif/translations/ru.json
+++ b/homeassistant/components/onvif/translations/ru.json
@@ -18,6 +18,16 @@
},
"title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f"
},
+ "configure": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ },
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF"
+ },
"configure_profile": {
"data": {
"include": "\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u044b"
@@ -40,6 +50,9 @@
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF"
},
"user": {
+ "data": {
+ "auto": "\u0418\u0441\u043a\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438"
+ },
"description": "\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u043d\u0430\u0436\u043c\u0451\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c, \u043d\u0430\u0447\u043d\u0451\u0442\u0441\u044f \u043f\u043e\u0438\u0441\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 ONVIF, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 Profile S.\n\n\u041d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u0438 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u044e\u0442 ONVIF. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e ONVIF \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0412\u0430\u0448\u0435\u0439 \u043a\u0430\u043c\u0435\u0440\u044b.",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF"
}
diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json
index 9450b3e9569..fa4d7d632da 100644
--- a/homeassistant/components/onvif/translations/zh-Hant.json
+++ b/homeassistant/components/onvif/translations/zh-Hant.json
@@ -18,6 +18,16 @@
},
"title": "\u8a2d\u5b9a\u9a57\u8b49"
},
+ "configure": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "name": "\u540d\u7a31",
+ "password": "\u5bc6\u78bc",
+ "port": "\u901a\u8a0a\u57e0",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ },
+ "title": "\u8a2d\u5b9a ONVIF \u88dd\u7f6e"
+ },
"configure_profile": {
"data": {
"include": "\u65b0\u589e\u651d\u5f71\u6a5f\u5be6\u9ad4"
@@ -40,6 +50,9 @@
"title": "\u8a2d\u5b9a ONVIF \u88dd\u7f6e"
},
"user": {
+ "data": {
+ "auto": "\u81ea\u52d5\u641c\u5c0b"
+ },
"description": "\u9ede\u4e0b\u50b3\u9001\u5f8c\u3001\u5c07\u6703\u641c\u5c0b\u7db2\u8def\u4e2d\u652f\u63f4 Profile S \u7684 ONVIF \u88dd\u7f6e\u3002\n\n\u67d0\u4e9b\u5ee0\u5546\u9810\u8a2d\u7684\u6a21\u5f0f\u70ba ONVIF \u95dc\u9589\u6a21\u5f0f\uff0c\u8acb\u518d\u6b21\u78ba\u8a8d\u651d\u5f71\u6a5f\u5df2\u7d93\u958b\u555f ONVIF\u3002",
"title": "ONVIF \u88dd\u7f6e\u8a2d\u5b9a"
}
diff --git a/homeassistant/components/opentherm_gw/translations/he.json b/homeassistant/components/opentherm_gw/translations/he.json
new file mode 100644
index 00000000000..eddeffa2ed0
--- /dev/null
+++ b/homeassistant/components/opentherm_gw/translations/he.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "id_exists": "\u05de\u05d6\u05d4\u05d4 \u05d4\u05e9\u05e2\u05e8 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05dd"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "device": "\u05e0\u05ea\u05d9\u05d1 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8",
+ "id": "\u05de\u05d6\u05d4\u05d4",
+ "name": "\u05e9\u05dd"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "temporary_override_mode": "\u05de\u05e6\u05d1 \u05e2\u05e7\u05d9\u05e4\u05d4 \u05d6\u05de\u05e0\u05d9 \u05e9\u05dc \u05e0\u05e7\u05d5\u05d3\u05ea \u05e2\u05e8\u05db\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/translations/pl.json b/homeassistant/components/opentherm_gw/translations/pl.json
index 9e1c4b1363d..69dd6040093 100644
--- a/homeassistant/components/opentherm_gw/translations/pl.json
+++ b/homeassistant/components/opentherm_gw/translations/pl.json
@@ -22,7 +22,8 @@
"data": {
"floor_temperature": "Zaokr\u0105glanie warto\u015bci w d\u00f3\u0142",
"read_precision": "Odczytaj precyzj\u0119",
- "set_precision": "Ustaw precyzj\u0119"
+ "set_precision": "Ustaw precyzj\u0119",
+ "temporary_override_mode": "Tryb tymczasowej zmiany nastawy"
},
"description": "Opcje dla bramki OpenTherm"
}
diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py
index 2ed6b56d914..e31cef9ee0a 100644
--- a/homeassistant/components/openuv/config_flow.py
+++ b/homeassistant/components/openuv/config_flow.py
@@ -14,26 +14,35 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv
from .const import DOMAIN
-CONFIG_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_API_KEY): str,
- vol.Inclusive(CONF_LATITUDE, "coords"): cv.latitude,
- vol.Inclusive(CONF_LONGITUDE, "coords"): cv.longitude,
- vol.Optional(CONF_ELEVATION): vol.Coerce(float),
- }
-)
-
class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle an OpenUV config flow."""
VERSION = 2
+ @property
+ def config_schema(self):
+ """Return the config schema."""
+ return vol.Schema(
+ {
+ vol.Required(CONF_API_KEY): str,
+ vol.Inclusive(
+ CONF_LATITUDE, "coords", default=self.hass.config.latitude
+ ): cv.latitude,
+ vol.Inclusive(
+ CONF_LONGITUDE, "coords", default=self.hass.config.longitude
+ ): cv.longitude,
+ vol.Optional(
+ CONF_ELEVATION, default=self.hass.config.elevation
+ ): vol.Coerce(float),
+ }
+ )
+
async def _show_form(self, errors=None):
"""Show the form to the user."""
return self.async_show_form(
step_id="user",
- data_schema=CONFIG_SCHEMA,
+ data_schema=self.config_schema,
errors=errors if errors else {},
)
diff --git a/homeassistant/components/openuv/translations/he.json b/homeassistant/components/openuv/translations/he.json
index 1a93fb4b438..90570023f05 100644
--- a/homeassistant/components/openuv/translations/he.json
+++ b/homeassistant/components/openuv/translations/he.json
@@ -1,13 +1,16 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
"error": {
"invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
},
"step": {
"user": {
"data": {
- "api_key": "\u05de\u05e4\u05ea\u05d7 API \u05e9\u05dc OpenUV",
- "elevation": "\u05d2\u05d5\u05d1\u05d4 \u05de\u05e2\u05dc \u05e4\u05e0\u05d9 \u05d4\u05d9\u05dd",
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "elevation": "\u05d2\u05d5\u05d1\u05d4",
"latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
"longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
},
diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py
index 49846a0ad0a..49bb870271e 100644
--- a/homeassistant/components/openweathermap/__init__.py
+++ b/homeassistant/components/openweathermap/__init__.py
@@ -30,14 +30,14 @@ from .weather_update_coordinator import WeatherUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up OpenWeatherMap as config entry."""
- name = config_entry.data[CONF_NAME]
- api_key = config_entry.data[CONF_API_KEY]
- latitude = config_entry.data.get(CONF_LATITUDE, hass.config.latitude)
- longitude = config_entry.data.get(CONF_LONGITUDE, hass.config.longitude)
- forecast_mode = _get_config_value(config_entry, CONF_MODE)
- language = _get_config_value(config_entry, CONF_LANGUAGE)
+ name = entry.data[CONF_NAME]
+ api_key = entry.data[CONF_API_KEY]
+ latitude = entry.data.get(CONF_LATITUDE, hass.config.latitude)
+ longitude = entry.data.get(CONF_LONGITUDE, hass.config.longitude)
+ forecast_mode = _get_config_value(entry, CONF_MODE)
+ language = _get_config_value(entry, CONF_LANGUAGE)
config_dict = _get_owm_config(language)
@@ -49,15 +49,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
await weather_coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
- hass.data[DOMAIN][config_entry.entry_id] = {
+ hass.data[DOMAIN][entry.entry_id] = {
ENTRY_NAME: name,
ENTRY_WEATHER_COORDINATOR: weather_coordinator,
}
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
- update_listener = config_entry.add_update_listener(async_update_options)
- hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER] = update_listener
+ update_listener = entry.add_update_listener(async_update_options)
+ hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener
return True
@@ -84,20 +84,18 @@ async def async_migrate_entry(hass, entry):
return True
-async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_update_options(hass: HomeAssistant, entry: ConfigEntry):
"""Update options."""
- await hass.config_entries.async_reload(config_entry.entry_id)
+ await hass.config_entries.async_reload(entry.entry_id)
-async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
- unload_ok = await hass.config_entries.async_unload_platforms(
- config_entry, PLATFORMS
- )
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
- update_listener = hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER]
+ update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER]
update_listener()
- hass.data[DOMAIN].pop(config_entry.entry_id)
+ hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
diff --git a/homeassistant/components/openweathermap/translations/he.json b/homeassistant/components/openweathermap/translations/he.json
index 4c49313d977..554fefee321 100644
--- a/homeassistant/components/openweathermap/translations/he.json
+++ b/homeassistant/components/openweathermap/translations/he.json
@@ -1,9 +1,30 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
"step": {
"user": {
"data": {
- "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "language": "\u05e9\u05e4\u05d4",
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
+ "mode": "\u05de\u05e6\u05d1"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "\u05e9\u05e4\u05d4",
+ "mode": "\u05de\u05e6\u05d1"
}
}
}
diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py
index 4518e3b6bda..98f39290d22 100644
--- a/homeassistant/components/openweathermap/weather_update_coordinator.py
+++ b/homeassistant/components/openweathermap/weather_update_coordinator.py
@@ -18,6 +18,7 @@ from homeassistant.components.weather import (
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
)
+from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers import sun
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt
@@ -179,10 +180,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
return forecast
- @staticmethod
- def _fmt_dewpoint(dewpoint):
+ def _fmt_dewpoint(self, dewpoint):
if dewpoint is not None:
- return round(dewpoint / 100, 1)
+ dewpoint = dewpoint - 273.15
+ return round(self.hass.config.units.temperature(dewpoint, TEMP_CELSIUS), 1)
return None
@staticmethod
diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py
index 063c0c6169d..c17873aefea 100644
--- a/homeassistant/components/oru/sensor.py
+++ b/homeassistant/components/oru/sensor.py
@@ -41,6 +41,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class CurrentEnergyUsageSensor(SensorEntity):
"""Representation of the sensor."""
+ _attr_icon = SENSOR_ICON
+ _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR
+
def __init__(self, meter):
"""Initialize the sensor."""
self._state = None
@@ -57,21 +60,11 @@ class CurrentEnergyUsageSensor(SensorEntity):
"""Return the name of the sensor."""
return SENSOR_NAME
- @property
- def icon(self):
- """Return the icon of the sensor."""
- return SENSOR_ICON
-
@property
def state(self):
"""Return the state of the sensor."""
return self._state
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return ENERGY_KILO_WATT_HOUR
-
def update(self):
"""Fetch new state data for the sensor."""
try:
diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json
index 6fccec14333..de86a7adf14 100644
--- a/homeassistant/components/ovo_energy/translations/de.json
+++ b/homeassistant/components/ovo_energy/translations/de.json
@@ -5,7 +5,7 @@
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung"
},
- "flow_title": "OVO Energy: {username}",
+ "flow_title": "{username}",
"step": {
"reauth": {
"data": {
diff --git a/homeassistant/components/ovo_energy/translations/he.json b/homeassistant/components/ovo_energy/translations/he.json
new file mode 100644
index 00000000000..7864218bc3b
--- /dev/null
+++ b/homeassistant/components/ovo_energy/translations/he.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\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"
+ },
+ "flow_title": "{username}",
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ },
+ "description": "\u05d4\u05d2\u05d3\u05e8 \u05de\u05d5\u05e4\u05e2 OVO \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d2\u05e9\u05ea \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e9\u05dc\u05da."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json
index 7bfd337cce5..14b3b23b2e7 100644
--- a/homeassistant/components/ovo_energy/translations/hu.json
+++ b/homeassistant/components/ovo_energy/translations/hu.json
@@ -5,7 +5,7 @@
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
- "flow_title": "OVO Energy: {username}",
+ "flow_title": "{username}",
"step": {
"reauth": {
"data": {
diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json
index 0bc533c0469..da313efbe6e 100644
--- a/homeassistant/components/owntracks/translations/de.json
+++ b/homeassistant/components/owntracks/translations/de.json
@@ -4,7 +4,7 @@
"single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"create_entry": {
- "default": "\n\n\u00d6ffnen unter Android [die OwnTracks-App]({android_url}) und gehe zu {android_url} - > Verbindung. \u00c4nder die folgenden Einstellungen: \n - Modus: Privates HTTP \n - Host: {webhook_url} \n - Identifizierung: \n - Benutzername: `''` \n - Ger\u00e4te-ID: `''` \n\n\u00d6ffnen unter iOS [die OwnTracks-App]({ios_url}) und tippe auf das Symbol (i) oben links - > Einstellungen. \u00c4nder die folgenden Einstellungen: \n - Modus: HTTP \n - URL: {webhook_url} \n - Aktivieren Sie die Authentifizierung \n - UserID: `''`\n\n {secret} \n \n Weitere Informationen findest du in der [Dokumentation]({docs_url})."
+ "default": "Unter Android \u00f6ffnen Sie [die OwnTracks App]({android_url}), gehen Sie zu Einstellungen -> Verbindung. \u00c4ndern Sie die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffnen Sie [die OwnTracks App]({ios_url}), tippen Sie auf das (i)-Symbol oben links -> Einstellungen. \u00c4ndern Sie die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen finden Sie in [der Dokumentation]({docs_url}).\n\n\u00dcbersetzt mit www.DeepL.com/Translator (kostenlose Version)"
},
"step": {
"user": {
diff --git a/homeassistant/components/owntracks/translations/he.json b/homeassistant/components/owntracks/translations/he.json
new file mode 100644
index 00000000000..d0c3523da94
--- /dev/null
+++ b/homeassistant/components/owntracks/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/ozw/__init__.py b/homeassistant/components/ozw/__init__.py
index 7890129dd91..6d9b474977d 100644
--- a/homeassistant/components/ozw/__init__.py
+++ b/homeassistant/components/ozw/__init__.py
@@ -56,7 +56,9 @@ DATA_DEVICES = "zwave-mqtt-devices"
DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client"
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # noqa: C901
+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] = {}
@@ -298,7 +300,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # noqa: C
return True
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
# cleanup platforms
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/ozw/light.py b/homeassistant/components/ozw/light.py
index b5fffbcf34f..7c52da23fb4 100644
--- a/homeassistant/components/ozw/light.py
+++ b/homeassistant/components/ozw/light.py
@@ -5,14 +5,14 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
+ ATTR_RGBW_COLOR,
ATTR_TRANSITION,
- ATTR_WHITE_VALUE,
+ COLOR_MODE_BRIGHTNESS,
+ COLOR_MODE_COLOR_TEMP,
+ COLOR_MODE_HS,
+ COLOR_MODE_RGBW,
DOMAIN as LIGHT_DOMAIN,
- SUPPORT_BRIGHTNESS,
- SUPPORT_COLOR,
- SUPPORT_COLOR_TEMP,
SUPPORT_TRANSITION,
- SUPPORT_WHITE_VALUE,
LightEntity,
)
from homeassistant.core import callback
@@ -65,9 +65,11 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
super().__init__(values)
self._color_channels = None
self._hs = None
- self._white = None
+ self._rgbw_color = None
self._ct = None
- self._supported_features = SUPPORT_BRIGHTNESS
+ 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
@@ -78,23 +80,29 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
def on_value_update(self):
"""Call when the underlying value(s) is added or updated."""
if self.values.dimming_duration is not None:
- self._supported_features |= SUPPORT_TRANSITION
-
- if self.values.color is not None:
- self._supported_features |= SUPPORT_COLOR
+ 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._supported_features |= SUPPORT_COLOR_TEMP
+ 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._supported_features |= SUPPORT_WHITE_VALUE
+ 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()
@@ -116,20 +124,15 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
return self.values.target.value > 0
return self.values.primary.value > 0
- @property
- def supported_features(self):
- """Flag supported features."""
- return self._supported_features
-
@property
def hs_color(self):
"""Return the hs color."""
return self._hs
@property
- def white_value(self):
- """Return the white value of this light between 0..255."""
- return self._white
+ def rgbw_color(self):
+ """Return the rgbw color."""
+ return self._rgbw_color
@property
def color_temp(self):
@@ -196,8 +199,8 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
self.async_set_duration(**kwargs)
rgbw = None
- white = kwargs.get(ATTR_WHITE_VALUE)
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:
@@ -211,12 +214,16 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
rgbw += "00"
# white LED must be off in order for color to work
- elif white is not None:
+ 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"#000000{white:02x}"
+ rgbw = f"#{red:02x}{green:02x}{blue:02x}{white:02x}"
else:
- rgbw = f"#00000000{white:02x}"
+ rgbw = f"#{red:02x}{green:02x}{blue:02x}00{white:02x}"
elif color_temp is not None:
# Limit color temp to min/max values
@@ -262,6 +269,9 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
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
@@ -286,15 +296,21 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
# Warm white
if self._color_channels & COLOR_CHANNEL_WARM_WHITE:
- self._white = int(data[index : index + 2], 16)
- temp_warm = self._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:
- self._white = int(data[index : index + 2], 16)
- temp_cold = self._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:
@@ -303,6 +319,17 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
- ((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
diff --git a/homeassistant/components/ozw/translations/he.json b/homeassistant/components/ozw/translations/he.json
new file mode 100644
index 00000000000..10f3cb6d722
--- /dev/null
+++ b/homeassistant/components/ozw/translations/he.json
@@ -0,0 +1,23 @@
+{
+ "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",
+ "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/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py
index 8f0a0e89d45..b217be4d4b6 100644
--- a/homeassistant/components/panasonic_viera/__init__.py
+++ b/homeassistant/components/panasonic_viera/__init__.py
@@ -164,7 +164,7 @@ class Remote:
if during_setup:
await self.async_update()
- except (TimeoutError, URLError, SOAPError, OSError) as err:
+ except (URLError, SOAPError, OSError) as err:
_LOGGER.debug("Could not establish remote connection: %s", err)
self._control = None
self.state = STATE_OFF
@@ -251,7 +251,7 @@ class Remote:
self.state = STATE_OFF
self.available = True
await self.async_create_remote_control()
- except (TimeoutError, URLError, OSError):
+ except (URLError, OSError):
self.state = STATE_OFF
self.available = self._on_action is not None
await self.async_create_remote_control()
diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py
index 93c33deb4dc..42400e7348c 100644
--- a/homeassistant/components/panasonic_viera/config_flow.py
+++ b/homeassistant/components/panasonic_viera/config_flow.py
@@ -56,7 +56,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._data[ATTR_DEVICE_INFO] = await self.hass.async_add_executor_job(
self._remote.get_device_info
)
- except (TimeoutError, URLError, SOAPError, OSError) as err:
+ except (URLError, SOAPError, OSError) as err:
_LOGGER.error("Could not establish remote connection: %s", err)
errors["base"] = "cannot_connect"
except Exception as err: # pylint: disable=broad-except
@@ -114,7 +114,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except SOAPError as err:
_LOGGER.error("Invalid PIN code: %s", err)
errors["base"] = ERROR_INVALID_PIN_CODE
- except (TimeoutError, URLError, OSError) as err:
+ except (URLError, OSError) as err:
_LOGGER.error("The remote connection was lost: %s", err)
return self.async_abort(reason="cannot_connect")
except Exception as err: # pylint: disable=broad-except
@@ -138,7 +138,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.hass.async_add_executor_job(
partial(self._remote.request_pin_code, name="Home Assistant")
)
- except (TimeoutError, URLError, SOAPError, OSError) as err:
+ except (URLError, SOAPError, OSError) as err:
_LOGGER.error("The remote connection was lost: %s", err)
return self.async_abort(reason="cannot_connect")
except Exception as err: # pylint: disable=broad-except
diff --git a/homeassistant/components/panasonic_viera/translations/he.json b/homeassistant/components/panasonic_viera/translations/he.json
new file mode 100644
index 00000000000..f19da6f07e4
--- /dev/null
+++ b/homeassistant/components/panasonic_viera/translations/he.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "pairing": {
+ "data": {
+ "pin": "\u05e7\u05d5\u05d3 PIN"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "name": "\u05e9\u05dd"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py
index a2e5dd4cbc2..1006df699f4 100644
--- a/homeassistant/components/philips_js/__init__.py
+++ b/homeassistant/components/philips_js/__init__.py
@@ -20,14 +20,14 @@ from homeassistant.core import CALLBACK_TYPE, Context, HassJob, HomeAssistant, c
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
-from .const import DOMAIN
+from .const import CONF_ALLOW_NOTIFY, DOMAIN
-PLATFORMS = ["media_player", "remote"]
+PLATFORMS = ["media_player", "light", "remote"]
LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Philips TV from a config entry."""
tvapi = PhilipsTV(
@@ -36,8 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
username=entry.data.get(CONF_USERNAME),
password=entry.data.get(CONF_PASSWORD),
)
-
- coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi)
+ coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi, entry.options)
await coordinator.async_refresh()
hass.data.setdefault(DOMAIN, {})
@@ -45,9 +44,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+ entry.async_on_unload(entry.add_update_listener(async_update_entry))
+
return True
+async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
+ """Update options."""
+ await hass.config_entries.async_reload(entry.entry_id)
+
+
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -94,9 +100,10 @@ class PluggableAction:
class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""Coordinator to update data."""
- def __init__(self, hass, api: PhilipsTV) -> None:
+ def __init__(self, hass, api: PhilipsTV, options: dict) -> None:
"""Set up the coordinator."""
self.api = api
+ self.options = options
self._notify_future: asyncio.Task | None = None
@callback
@@ -127,6 +134,7 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
self.api.on
and self.api.powerstate == "On"
and self.api.notify_change_supported
+ and self.options.get(CONF_ALLOW_NOTIFY, False)
)
async def _notify_task(self):
diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py
index 84303e6ca92..59403b2ec86 100644
--- a/homeassistant/components/philips_js/config_flow.py
+++ b/homeassistant/components/philips_js/config_flow.py
@@ -17,7 +17,7 @@ from homeassistant.const import (
)
from . import LOGGER
-from .const import CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, DOMAIN
+from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, DOMAIN
async def validate_input(
@@ -154,3 +154,32 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
}
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
+
+ @staticmethod
+ @core.callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return OptionsFlowHandler(config_entry)
+
+
+class OptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle a option flow for AEMET."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Handle options flow."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ data_schema = vol.Schema(
+ {
+ vol.Required(
+ CONF_ALLOW_NOTIFY,
+ default=self.config_entry.options.get(CONF_ALLOW_NOTIFY),
+ ): bool,
+ }
+ )
+ return self.async_show_form(step_id="init", data_schema=data_schema)
diff --git a/homeassistant/components/philips_js/const.py b/homeassistant/components/philips_js/const.py
index 5769a8979ce..5d1141a8fb9 100644
--- a/homeassistant/components/philips_js/const.py
+++ b/homeassistant/components/philips_js/const.py
@@ -2,6 +2,7 @@
DOMAIN = "philips_js"
CONF_SYSTEM = "system"
+CONF_ALLOW_NOTIFY = "allow_notify"
CONST_APP_ID = "homeassistant.io"
CONST_APP_NAME = "Home Assistant"
diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py
index 77782fc641c..51efa643310 100644
--- a/homeassistant/components/philips_js/device_trigger.py
+++ b/homeassistant/components/philips_js/device_trigger.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry
@@ -16,7 +16,7 @@ from .const import DOMAIN
TRIGGER_TYPE_TURN_ON = "turn_on"
TRIGGER_TYPES = {TRIGGER_TYPE_TURN_ON}
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
}
@@ -45,16 +45,16 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE | None:
"""Attach a trigger."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
registry: DeviceRegistry = await async_get_registry(hass)
if config[CONF_TYPE] == TRIGGER_TYPE_TURN_ON:
variables = {
"trigger": {
+ **trigger_data,
"platform": "device",
"domain": DOMAIN,
"device_id": config[CONF_DEVICE_ID],
"description": f"philips_js '{config[CONF_TYPE]}' event",
- "id": trigger_id,
}
}
diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py
new file mode 100644
index 00000000000..4c321468d79
--- /dev/null
+++ b/homeassistant/components/philips_js/light.py
@@ -0,0 +1,385 @@
+"""Component to integrate ambilight for TVs exposing the Joint Space API."""
+from __future__ import annotations
+
+from typing import Any
+
+from haphilipsjs import PhilipsTV
+from haphilipsjs.typing import AmbilightCurrentConfiguration
+
+from homeassistant import config_entries
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_EFFECT,
+ ATTR_HS_COLOR,
+ COLOR_MODE_HS,
+ COLOR_MODE_ONOFF,
+ SUPPORT_BRIGHTNESS,
+ SUPPORT_COLOR,
+ SUPPORT_EFFECT,
+ LightEntity,
+)
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+from homeassistant.util.color import color_hsv_to_RGB, color_RGB_to_hsv
+
+from . import PhilipsTVDataUpdateCoordinator
+from .const import CONF_SYSTEM, DOMAIN
+
+EFFECT_PARTITION = ": "
+EFFECT_MODE = "Mode"
+EFFECT_EXPERT = "Expert"
+EFFECT_AUTO = "Auto"
+EFFECT_EXPERT_STYLES = {"FOLLOW_AUDIO", "FOLLOW_COLOR", "Lounge light"}
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ config_entry: config_entries.ConfigEntry,
+ async_add_entities,
+):
+ """Set up the configuration entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]
+ async_add_entities(
+ [
+ PhilipsTVLightEntity(
+ coordinator, config_entry.data[CONF_SYSTEM], config_entry.unique_id
+ )
+ ]
+ )
+
+
+def _get_settings(style: AmbilightCurrentConfiguration):
+ """Extract the color settings data from a style."""
+ if style["styleName"] in ("FOLLOW_COLOR", "Lounge light"):
+ return style["colorSettings"]
+ if style["styleName"] == "FOLLOW_AUDIO":
+ return style["audioSettings"]
+ return None
+
+
+def _parse_effect(effect: str):
+ style, _, algorithm = effect.partition(EFFECT_PARTITION)
+ if style == EFFECT_MODE:
+ return EFFECT_MODE, algorithm, None
+ algorithm, _, expert = algorithm.partition(EFFECT_PARTITION)
+ if expert:
+ return EFFECT_EXPERT, style, algorithm
+ return EFFECT_AUTO, style, algorithm
+
+
+def _get_effect(mode: str, style: str, algorithm: str | None):
+ if mode == EFFECT_MODE:
+ return f"{EFFECT_MODE}{EFFECT_PARTITION}{style}"
+ if mode == EFFECT_EXPERT:
+ return f"{style}{EFFECT_PARTITION}{algorithm}{EFFECT_PARTITION}{EFFECT_EXPERT}"
+ return f"{style}{EFFECT_PARTITION}{algorithm}"
+
+
+def _is_on(mode, style, powerstate):
+ if mode in (EFFECT_AUTO, EFFECT_EXPERT):
+ if style in ("FOLLOW_VIDEO", "FOLLOW_AUDIO"):
+ return powerstate in ("On", None)
+ if style == "OFF":
+ return False
+ return True
+
+ if mode == EFFECT_MODE:
+ if style == "internal":
+ return powerstate in ("On", None)
+ return True
+
+ return False
+
+
+def _is_valid(mode, style):
+ if mode == EFFECT_EXPERT:
+ return style in EFFECT_EXPERT_STYLES
+ return True
+
+
+def _get_cache_keys(device: PhilipsTV):
+ """Return a cache keys to avoid always updating."""
+ return (
+ device.on,
+ device.powerstate,
+ device.ambilight_current_configuration,
+ device.ambilight_mode,
+ )
+
+
+def _average_pixels(data):
+ """Calculate an average color over all ambilight pixels."""
+ color_c = 0
+ color_r = 0.0
+ color_g = 0.0
+ color_b = 0.0
+ for layer in data.values():
+ for side in layer.values():
+ for pixel in side.values():
+ color_c += 1
+ color_r += pixel["r"]
+ color_g += pixel["g"]
+ color_b += pixel["b"]
+
+ if color_c:
+ color_r /= color_c
+ color_g /= color_c
+ color_b /= color_c
+ return color_r, color_g, color_b
+ return 0.0, 0.0, 0.0
+
+
+class PhilipsTVLightEntity(CoordinatorEntity, LightEntity):
+ """Representation of a Philips TV exposing the JointSpace API."""
+
+ def __init__(
+ self,
+ coordinator: PhilipsTVDataUpdateCoordinator,
+ system: dict[str, Any],
+ unique_id: str,
+ ) -> None:
+ """Initialize light."""
+ self._tv = coordinator.api
+ self._hs = None
+ self._brightness = None
+ self._system = system
+ self._coordinator = coordinator
+ self._cache_keys = None
+ super().__init__(coordinator)
+
+ self._attr_supported_color_modes = [COLOR_MODE_HS, COLOR_MODE_ONOFF]
+ self._attr_supported_features = (
+ SUPPORT_EFFECT | SUPPORT_COLOR | SUPPORT_BRIGHTNESS
+ )
+ self._attr_name = self._system["name"]
+ self._attr_unique_id = unique_id
+ self._attr_icon = "mdi:television-ambient-light"
+ self._attr_device_info = {
+ "name": self._system["name"],
+ "identifiers": {
+ (DOMAIN, self._attr_unique_id),
+ },
+ "model": self._system.get("model"),
+ "manufacturer": "Philips",
+ "sw_version": self._system.get("softwareversion"),
+ }
+
+ self._update_from_coordinator()
+
+ def _calculate_effect_list(self):
+ """Calculate an effect list based on current status."""
+ effects = []
+ effects.extend(
+ _get_effect(EFFECT_AUTO, style, setting)
+ for style, data in self._tv.ambilight_styles.items()
+ if _is_valid(EFFECT_AUTO, style)
+ and _is_on(EFFECT_AUTO, style, self._tv.powerstate)
+ for setting in data.get("menuSettings", [])
+ )
+
+ effects.extend(
+ _get_effect(EFFECT_EXPERT, style, algorithm)
+ for style, data in self._tv.ambilight_styles.items()
+ if _is_valid(EFFECT_EXPERT, style)
+ and _is_on(EFFECT_EXPERT, style, self._tv.powerstate)
+ for algorithm in data.get("algorithms", [])
+ )
+
+ effects.extend(
+ _get_effect(EFFECT_MODE, style, None)
+ for style in self._tv.ambilight_modes
+ if _is_valid(EFFECT_MODE, style)
+ and _is_on(EFFECT_MODE, style, self._tv.powerstate)
+ )
+
+ return sorted(effects)
+
+ def _calculate_effect(self):
+ """Return the current effect."""
+ current = self._tv.ambilight_current_configuration
+ if current and self._tv.ambilight_mode != "manual":
+ if current["isExpert"]:
+ settings = _get_settings(current)
+ if settings:
+ return _get_effect(
+ EFFECT_EXPERT, current["styleName"], settings["algorithm"]
+ )
+ return _get_effect(EFFECT_EXPERT, current["styleName"], None)
+
+ return _get_effect(
+ EFFECT_AUTO, current["styleName"], current.get("menuSetting", None)
+ )
+
+ return _get_effect(EFFECT_MODE, self._tv.ambilight_mode, None)
+
+ @property
+ def color_mode(self):
+ """Return the current color mode."""
+ current = self._tv.ambilight_current_configuration
+ if current and current["isExpert"]:
+ return COLOR_MODE_HS
+
+ if self._tv.ambilight_mode in ["manual", "expert"]:
+ return COLOR_MODE_HS
+
+ return COLOR_MODE_ONOFF
+
+ @property
+ def is_on(self):
+ """Return if the light is turned on."""
+ if self._tv.on:
+ mode, style, _ = _parse_effect(self.effect)
+ return _is_on(mode, style, self._tv.powerstate)
+
+ return False
+
+ def _update_from_coordinator(self):
+ current = self._tv.ambilight_current_configuration
+ color = None
+
+ if (cache_keys := _get_cache_keys(self._tv)) != self._cache_keys:
+ self._cache_keys = cache_keys
+ self._attr_effect_list = self._calculate_effect_list()
+ self._attr_effect = self._calculate_effect()
+
+ if current and current["isExpert"]:
+ if settings := _get_settings(current):
+ color = settings["color"]
+
+ mode, _, _ = _parse_effect(self._attr_effect)
+
+ if mode == EFFECT_EXPERT and color:
+ self._attr_hs_color = (
+ color["hue"] * 360.0 / 255.0,
+ color["saturation"] * 100.0 / 255.0,
+ )
+ self._attr_brightness = color["brightness"]
+ elif mode == EFFECT_MODE and self._tv.ambilight_cached:
+ hsv_h, hsv_s, hsv_v = color_RGB_to_hsv(
+ *_average_pixels(self._tv.ambilight_cached)
+ )
+ self._attr_hs_color = hsv_h, hsv_s
+ self._attr_brightness = hsv_v * 255.0 / 100.0
+ else:
+ self._attr_hs_color = None
+ self._attr_brightness = None
+
+ @callback
+ def _handle_coordinator_update(self) -> None:
+ """Handle updated data from the coordinator."""
+ self._update_from_coordinator()
+ super()._handle_coordinator_update()
+
+ async def _set_ambilight_cached(self, algorithm, hs_color, brightness):
+ """Set ambilight via the manual or expert mode."""
+ rgb = color_hsv_to_RGB(hs_color[0], hs_color[1], brightness * 100 / 255)
+
+ data = {
+ "r": rgb[0],
+ "g": rgb[1],
+ "b": rgb[2],
+ }
+
+ if not await self._tv.setAmbilightCached(data):
+ raise Exception("Failed to set ambilight color")
+
+ if algorithm != self._tv.ambilight_mode:
+ if not await self._tv.setAmbilightMode(algorithm):
+ raise Exception("Failed to set ambilight mode")
+
+ async def _set_ambilight_expert_config(
+ self, style, algorithm, hs_color, brightness
+ ):
+ """Set ambilight via current configuration."""
+ config: AmbilightCurrentConfiguration = {
+ "styleName": style,
+ "isExpert": True,
+ }
+
+ setting = {
+ "algorithm": algorithm,
+ "color": {
+ "hue": round(hs_color[0] * 255.0 / 360.0),
+ "saturation": round(hs_color[1] * 255.0 / 100.0),
+ "brightness": round(brightness),
+ },
+ "colorDelta": {
+ "hue": 0,
+ "saturation": 0,
+ "brightness": 0,
+ },
+ }
+
+ if style in ("FOLLOW_COLOR", "Lounge light"):
+ config["colorSettings"] = setting
+ config["speed"] = 2
+
+ elif style == "FOLLOW_AUDIO":
+ config["audioSettings"] = setting
+ config["tuning"] = 0
+
+ if not await self._tv.setAmbilightCurrentConfiguration(config):
+ raise Exception("Failed to set ambilight mode")
+
+ async def _set_ambilight_config(self, style, algorithm):
+ """Set ambilight via current configuration."""
+ config: AmbilightCurrentConfiguration = {
+ "styleName": style,
+ "isExpert": False,
+ "menuSetting": algorithm,
+ }
+
+ if await self._tv.setAmbilightCurrentConfiguration(config) is False:
+ raise Exception("Failed to set ambilight mode")
+
+ async def async_turn_on(self, **kwargs) -> None:
+ """Turn the bulb on."""
+ brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness)
+ hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color)
+ effect = kwargs.get(ATTR_EFFECT, self.effect)
+
+ if not self._tv.on:
+ raise Exception("TV is not available")
+
+ mode, style, setting = _parse_effect(effect)
+
+ if not _is_on(mode, style, self._tv.powerstate):
+ mode = EFFECT_MODE
+ setting = None
+ if self._tv.powerstate in ("On", None):
+ style = "internal"
+ else:
+ style = "manual"
+
+ if brightness is None:
+ brightness = 255
+
+ if hs_color is None:
+ hs_color = [0, 0]
+
+ if mode == EFFECT_MODE:
+ await self._set_ambilight_cached(style, hs_color, brightness)
+ elif mode == EFFECT_AUTO:
+ await self._set_ambilight_config(style, setting)
+ elif mode == EFFECT_EXPERT:
+ await self._set_ambilight_expert_config(
+ style, setting, hs_color, brightness
+ )
+
+ self._update_from_coordinator()
+ self.async_write_ha_state()
+
+ async def async_turn_off(self, **kwargs) -> None:
+ """Turn of ambilight."""
+
+ if not self._tv.on:
+ raise Exception("TV is not available")
+
+ if await self._tv.setAmbilightMode("internal") is False:
+ raise Exception("Failed to set ambilight mode")
+
+ await self._set_ambilight_config("OFF", "")
+
+ self._update_from_coordinator()
+ self.async_write_ha_state()
diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json
index 9d1c4dbd04d..4f3ee5a9ab3 100644
--- a/homeassistant/components/philips_js/manifest.json
+++ b/homeassistant/components/philips_js/manifest.json
@@ -2,7 +2,7 @@
"domain": "philips_js",
"name": "Philips TV",
"documentation": "https://www.home-assistant.io/integrations/philips_js",
- "requirements": ["ha-philipsjs==2.7.3"],
+ "requirements": ["ha-philipsjs==2.7.4"],
"codeowners": ["@elupus"],
"config_flow": true,
"iot_class": "local_polling"
diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py
index 61aa97a66b1..e4512fc52f0 100644
--- a/homeassistant/components/philips_js/media_player.py
+++ b/homeassistant/components/philips_js/media_player.py
@@ -123,6 +123,8 @@ async def async_setup_entry(
class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
"""Representation of a Philips TV exposing the JointSpace API."""
+ _attr_device_class = DEVICE_CLASS_TV
+
def __init__(
self,
coordinator: PhilipsTVDataUpdateCoordinator,
@@ -315,11 +317,6 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
if app:
return app.get("label")
- @property
- def device_class(self):
- """Return the device class."""
- return DEVICE_CLASS_TV
-
@property
def unique_id(self):
"""Return unique identifier if known."""
diff --git a/homeassistant/components/philips_js/strings.json b/homeassistant/components/philips_js/strings.json
index 5c8f08eff6a..3e6d4f494d3 100644
--- a/homeassistant/components/philips_js/strings.json
+++ b/homeassistant/components/philips_js/strings.json
@@ -20,11 +20,20 @@
"unknown": "[%key:common::config_flow::error::unknown%]",
"pairing_failure": "Unable to pair: {error_id}",
"invalid_pin": "Invalid PIN"
-},
+ },
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "allow_notify": "Allow usage of data notification service."
+ }
+ }
+ }
+ },
"device_automation": {
"trigger_type": {
"turn_on": "Device is requested to turn on"
diff --git a/homeassistant/components/philips_js/translations/de.json b/homeassistant/components/philips_js/translations/de.json
index 552d7aca07a..67d87b32001 100644
--- a/homeassistant/components/philips_js/translations/de.json
+++ b/homeassistant/components/philips_js/translations/de.json
@@ -29,5 +29,14 @@
"trigger_type": {
"turn_on": "Ger\u00e4t wird zum Einschalten aufgefordert"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "allow_notify": "Nutzung des Datenbenachrichtigungsdienstes zulassen."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/en.json b/homeassistant/components/philips_js/translations/en.json
index ea254a3873d..1519bf440ee 100644
--- a/homeassistant/components/philips_js/translations/en.json
+++ b/homeassistant/components/philips_js/translations/en.json
@@ -29,5 +29,14 @@
"trigger_type": {
"turn_on": "Device is requested to turn on"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "allow_notify": "Allow usage of data notification service."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/et.json b/homeassistant/components/philips_js/translations/et.json
index 4a5bf9fe6e9..a089b7b0e57 100644
--- a/homeassistant/components/philips_js/translations/et.json
+++ b/homeassistant/components/philips_js/translations/et.json
@@ -29,5 +29,14 @@
"trigger_type": {
"turn_on": "Seadmel palutakse sisse l\u00fclituda"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "allow_notify": "Luba teavitusteenused."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/he.json b/homeassistant/components/philips_js/translations/he.json
index 04648fe5845..499f76c059e 100644
--- a/homeassistant/components/philips_js/translations/he.json
+++ b/homeassistant/components/philips_js/translations/he.json
@@ -1,7 +1,25 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
"error": {
- "pairing_failure": "\u05e6\u05d9\u05de\u05d5\u05d3 \u05e0\u05db\u05e9\u05dc"
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "pairing_failure": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e9\u05d9\u05d9\u05da: {error_id}",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "\u05e7\u05d5\u05d3 PIN"
+ },
+ "title": "\u05d6\u05d9\u05d5\u05d5\u05d2"
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/nl.json b/homeassistant/components/philips_js/translations/nl.json
index 34497d285fa..0b172e24f56 100644
--- a/homeassistant/components/philips_js/translations/nl.json
+++ b/homeassistant/components/philips_js/translations/nl.json
@@ -29,5 +29,14 @@
"trigger_type": {
"turn_on": "Apparaat wordt gevraagd om in te schakelen"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "allow_notify": "Sta het gebruik van de gegevensnotificatieservice toe."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/no.json b/homeassistant/components/philips_js/translations/no.json
index 5b1df7c8e8b..732749bcba0 100644
--- a/homeassistant/components/philips_js/translations/no.json
+++ b/homeassistant/components/philips_js/translations/no.json
@@ -29,5 +29,14 @@
"trigger_type": {
"turn_on": "Enheten blir bedt om \u00e5 sl\u00e5 p\u00e5"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "allow_notify": "Tillat bruk av datavarslingstjeneste."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/ru.json b/homeassistant/components/philips_js/translations/ru.json
index df3dfd4b6f6..76597a21a9f 100644
--- a/homeassistant/components/philips_js/translations/ru.json
+++ b/homeassistant/components/philips_js/translations/ru.json
@@ -29,5 +29,14 @@
"trigger_type": {
"turn_on": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "allow_notify": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/zh-Hant.json b/homeassistant/components/philips_js/translations/zh-Hant.json
index de7f02b7a21..56509ac44dd 100644
--- a/homeassistant/components/philips_js/translations/zh-Hant.json
+++ b/homeassistant/components/philips_js/translations/zh-Hant.json
@@ -29,5 +29,14 @@
"trigger_type": {
"turn_on": "\u88dd\u7f6e\u5fc5\u9808\u70ba\u958b\u555f\u72c0\u614b"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "allow_notify": "\u5141\u8a31\u4f7f\u7528\u6578\u64da\u9032\u884c\u901a\u77e5\u670d\u52d9\u3002"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py
index 34fdc9978c1..ab9191b0f4a 100644
--- a/homeassistant/components/pi_hole/__init__.py
+++ b/homeassistant/components/pi_hole/__init__.py
@@ -1,11 +1,13 @@
"""The pi_hole component."""
+from __future__ import annotations
+
import logging
from hole import Hole
from hole.exceptions import HoleError
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_IMPORT
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
@@ -13,10 +15,12 @@ from homeassistant.const import (
CONF_SSL,
CONF_VERIFY_SSL,
)
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
@@ -60,7 +64,7 @@ CONFIG_SCHEMA = vol.Schema(
)
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Pi-hole integration."""
hass.data[DOMAIN] = {}
@@ -77,7 +81,7 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Pi-hole entry."""
name = entry.data[CONF_NAME]
host = entry.data[CONF_HOST]
@@ -109,7 +113,7 @@ async def async_setup_entry(hass, entry):
_LOGGER.warning("Failed to connect: %s", ex)
raise ConfigEntryNotReady from ex
- async def async_update_data():
+ async def async_update_data() -> None:
"""Fetch data from API endpoint."""
try:
await api.get_data()
@@ -133,7 +137,7 @@ async def async_setup_entry(hass, entry):
return True
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Pi-hole entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
entry, _async_platforms(entry)
@@ -144,7 +148,7 @@ async def async_unload_entry(hass, entry):
@callback
-def _async_platforms(entry):
+def _async_platforms(entry: ConfigEntry) -> list[str]:
"""Return platforms to be loaded / unloaded."""
platforms = ["sensor"]
if not entry.data[CONF_STATISTICS_ONLY]:
@@ -157,7 +161,13 @@ def _async_platforms(entry):
class PiHoleEntity(CoordinatorEntity):
"""Representation of a Pi-hole entity."""
- def __init__(self, api, coordinator, name, server_unique_id):
+ def __init__(
+ self,
+ api: Hole,
+ coordinator: DataUpdateCoordinator,
+ name: str,
+ server_unique_id: str,
+ ) -> None:
"""Initialize a Pi-hole entity."""
super().__init__(coordinator)
self.api = api
@@ -165,12 +175,12 @@ class PiHoleEntity(CoordinatorEntity):
self._server_unique_id = server_unique_id
@property
- def icon(self):
+ def icon(self) -> str:
"""Icon to use in the frontend, if any."""
return "mdi:pi-hole"
@property
- def device_info(self):
+ def device_info(self) -> DeviceInfo:
"""Return the device information of the entity."""
return {
"identifiers": {(DOMAIN, self._server_unique_id)},
diff --git a/homeassistant/components/pi_hole/binary_sensor.py b/homeassistant/components/pi_hole/binary_sensor.py
index 714a9f669c8..3c322d324d3 100644
--- a/homeassistant/components/pi_hole/binary_sensor.py
+++ b/homeassistant/components/pi_hole/binary_sensor.py
@@ -1,12 +1,17 @@
"""Support for getting status from a Pi-hole system."""
from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import PiHoleEntity
from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN
-async def async_setup_entry(hass, entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
"""Set up the Pi-hole binary sensor."""
name = entry.data[CONF_NAME]
hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id]
@@ -25,16 +30,16 @@ class PiHoleBinarySensor(PiHoleEntity, BinarySensorEntity):
"""Representation of a Pi-hole binary sensor."""
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
- def unique_id(self):
+ def unique_id(self) -> str:
"""Return the unique id of the sensor."""
return f"{self._server_unique_id}/Status"
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return if the service is on."""
- return self.api.data.get("status") == "enabled"
+ return self.api.data.get("status") == "enabled" # type: ignore[no-any-return]
diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py
index 68f0ecbbb2c..cccd80472e3 100644
--- a/homeassistant/components/pi_hole/config_flow.py
+++ b/homeassistant/components/pi_hole/config_flow.py
@@ -1,5 +1,8 @@
"""Config flow to configure the Pi-hole integration."""
+from __future__ import annotations
+
import logging
+from typing import Any
from hole import Hole
from hole.exceptions import HoleError
@@ -24,6 +27,7 @@ from homeassistant.const import (
CONF_SSL,
CONF_VERIFY_SSL,
)
+from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
_LOGGER = logging.getLogger(__name__)
@@ -34,19 +38,25 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
- def __init__(self):
+ def __init__(self) -> None:
"""Initialize the config flow."""
- self._config = None
+ self._config: dict = {}
- async def async_step_user(self, user_input=None):
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
"""Handle a flow initiated by the user."""
return await self.async_step_init(user_input)
- async def async_step_import(self, user_input=None):
+ async def async_step_import(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
"""Handle a flow initiated by import."""
return await self.async_step_init(user_input, is_import=True)
- async def async_step_init(self, user_input, is_import=False):
+ async def async_step_init(
+ self, user_input: dict[str, Any] | None, is_import: bool = False
+ ) -> FlowResult:
"""Handle init step of a flow."""
errors = {}
@@ -131,7 +141,9 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors,
)
- async def async_step_api_key(self, user_input=None):
+ async def async_step_api_key(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
"""Handle step to setup API key."""
if user_input is not None:
return self.async_create_entry(
@@ -147,14 +159,16 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema({vol.Optional(CONF_API_KEY): str}),
)
- async def _async_endpoint_existed(self, endpoint):
+ async def _async_endpoint_existed(self, endpoint: str) -> bool:
existing_endpoints = [
f"{entry.data.get(CONF_HOST)}/{entry.data.get(CONF_LOCATION)}"
for entry in self._async_current_entries()
]
return endpoint in existing_endpoints
- async def _async_try_connect(self, host, location, tls, verify_tls):
+ async def _async_try_connect(
+ self, host: str, location: str, tls: bool, verify_tls: bool
+ ) -> None:
session = async_get_clientsession(self.hass, verify_tls)
pi_hole = Hole(host, self.hass.loop, session, location=location, tls=tls)
await pi_hole.get_data()
diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py
index 517e8cfcf17..95aee56f7cc 100644
--- a/homeassistant/components/pi_hole/sensor.py
+++ b/homeassistant/components/pi_hole/sensor.py
@@ -1,7 +1,16 @@
"""Support for getting statistical data from a Pi-hole system."""
+from __future__ import annotations
+
+from typing import Any
+
+from hole import Hole
from homeassistant.components.sensor import SensorEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import PiHoleEntity
from .const import (
@@ -14,7 +23,9 @@ from .const import (
)
-async def async_setup_entry(hass, entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
"""Set up the Pi-hole sensor."""
name = entry.data[CONF_NAME]
hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id]
@@ -34,7 +45,14 @@ async def async_setup_entry(hass, entry, async_add_entities):
class PiHoleSensor(PiHoleEntity, SensorEntity):
"""Representation of a Pi-hole sensor."""
- def __init__(self, api, coordinator, name, sensor_name, server_unique_id):
+ def __init__(
+ self,
+ api: Hole,
+ coordinator: DataUpdateCoordinator,
+ name: str,
+ sensor_name: str,
+ server_unique_id: str,
+ ) -> None:
"""Initialize a Pi-hole sensor."""
super().__init__(api, coordinator, name, server_unique_id)
@@ -46,27 +64,27 @@ class PiHoleSensor(PiHoleEntity, SensorEntity):
self._icon = variable_info[2]
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the sensor."""
return f"{self._name} {self._condition_name}"
@property
- def unique_id(self):
+ def unique_id(self) -> str:
"""Return the unique id of the sensor."""
return f"{self._server_unique_id}/{self._condition_name}"
@property
- def icon(self):
+ def icon(self) -> str:
"""Icon to use in the frontend, if any."""
return self._icon
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> str:
"""Return the unit the value is expressed in."""
return self._unit_of_measurement
@property
- def state(self):
+ def state(self) -> Any:
"""Return the state of the device."""
try:
return round(self.api.data[self._condition], 2)
@@ -74,6 +92,6 @@ class PiHoleSensor(PiHoleEntity, SensorEntity):
return self.api.data[self._condition]
@property
- def extra_state_attributes(self):
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the Pi-hole."""
return {ATTR_BLOCKED_DOMAINS: self.api.data["domains_being_blocked"]}
diff --git a/homeassistant/components/pi_hole/switch.py b/homeassistant/components/pi_hole/switch.py
index 955585243cf..b0c4b09c2e7 100644
--- a/homeassistant/components/pi_hole/switch.py
+++ b/homeassistant/components/pi_hole/switch.py
@@ -1,12 +1,18 @@
"""Support for turning on and off Pi-hole system."""
+from __future__ import annotations
+
import logging
+from typing import Any
from hole.exceptions import HoleError
import voluptuous as vol
from homeassistant.components.switch import SwitchEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
+from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import PiHoleEntity
from .const import (
@@ -20,7 +26,9 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass, entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
"""Set up the Pi-hole switch."""
name = entry.data[CONF_NAME]
hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id]
@@ -51,26 +59,26 @@ class PiHoleSwitch(PiHoleEntity, SwitchEntity):
"""Representation of a Pi-hole switch."""
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the switch."""
return self._name
@property
- def unique_id(self):
+ def unique_id(self) -> str:
"""Return the unique id of the switch."""
return f"{self._server_unique_id}/Switch"
@property
- def icon(self):
+ def icon(self) -> str:
"""Icon to use in the frontend, if any."""
return "mdi:pi-hole"
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return if the service is on."""
- return self.api.data.get("status") == "enabled"
+ return self.api.data.get("status") == "enabled" # type: ignore[no-any-return]
- async def async_turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the service."""
try:
await self.api.enable()
@@ -78,11 +86,11 @@ class PiHoleSwitch(PiHoleEntity, SwitchEntity):
except HoleError as err:
_LOGGER.error("Unable to enable Pi-hole: %s", err)
- async def async_turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the service."""
await self.async_disable()
- async def async_disable(self, duration=None):
+ async def async_disable(self, duration: Any = None) -> None:
"""Disable the service for a given duration."""
duration_seconds = True # Disable infinitely by default
if duration is not None:
diff --git a/homeassistant/components/pi_hole/translations/he.json b/homeassistant/components/pi_hole/translations/he.json
new file mode 100644
index 00000000000..9b4392617f9
--- /dev/null
+++ b/homeassistant/components/pi_hole/translations/he.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "api_key": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API"
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "location": "\u05de\u05d9\u05e7\u05d5\u05dd",
+ "name": "\u05e9\u05dd",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py
index 055faadb784..1a2164dafed 100644
--- a/homeassistant/components/picnic/__init__.py
+++ b/homeassistant/components/picnic/__init__.py
@@ -20,7 +20,7 @@ def create_picnic_client(entry: ConfigEntry):
)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Picnic from a config entry."""
picnic_client = await hass.async_add_executor_job(create_picnic_client, entry)
picnic_coordinator = PicnicUpdateCoordinator(hass, picnic_client, entry)
diff --git a/homeassistant/components/picnic/translations/he.json b/homeassistant/components/picnic/translations/he.json
new file mode 100644
index 00000000000..f668538909b
--- /dev/null
+++ b/homeassistant/components/picnic/translations/he.json
@@ -0,0 +1,22 @@
+{
+ "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",
+ "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": {
+ "country_code": "\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05de\u05d3\u05d9\u05e0\u05d4",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ },
+ "title": "\u05e4\u05d9\u05e7\u05e0\u05d9\u05e7"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py
index b9a9f6460db..70b90ccd886 100644
--- a/homeassistant/components/ping/__init__.py
+++ b/homeassistant/components/ping/__init__.py
@@ -5,10 +5,9 @@ import logging
from icmplib import SocketPermissionError, ping as icmp_ping
-from homeassistant.core import callback
from homeassistant.helpers.reload import async_setup_reload_service
-from .const import DEFAULT_START_ID, DOMAIN, MAX_PING_ID, PING_ID, PING_PRIVS, PLATFORMS
+from .const import DOMAIN, PING_PRIVS, PLATFORMS
_LOGGER = logging.getLogger(__name__)
@@ -18,30 +17,10 @@ async def async_setup(hass, config):
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
hass.data[DOMAIN] = {
PING_PRIVS: await hass.async_add_executor_job(_can_use_icmp_lib_with_privilege),
- PING_ID: DEFAULT_START_ID,
}
return True
-@callback
-def async_get_next_ping_id(hass, count=1):
- """Find the next id to use in the outbound ping.
-
- When using multiping, we increment the id
- by the number of ids that multiping
- will use.
-
- Must be called in async
- """
- allocated_id = hass.data[DOMAIN][PING_ID] + 1
- if allocated_id > MAX_PING_ID:
- allocated_id -= MAX_PING_ID - DEFAULT_START_ID
- hass.data[DOMAIN][PING_ID] += count
- if hass.data[DOMAIN][PING_ID] > MAX_PING_ID:
- hass.data[DOMAIN][PING_ID] -= MAX_PING_ID - DEFAULT_START_ID
- return allocated_id
-
-
def _can_use_icmp_lib_with_privilege() -> None | bool:
"""Verify we can create a raw socket."""
try:
diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py
index 9ae891d598b..cf2d8f7ed7a 100644
--- a/homeassistant/components/ping/binary_sensor.py
+++ b/homeassistant/components/ping/binary_sensor.py
@@ -4,13 +4,12 @@ from __future__ import annotations
import asyncio
from contextlib import suppress
from datetime import timedelta
-from functools import partial
import logging
import re
import sys
from typing import Any
-from icmplib import NameLookupError, ping as icmp_ping
+from icmplib import NameLookupError, async_ping
import voluptuous as vol
from homeassistant.components.binary_sensor import (
@@ -22,7 +21,6 @@ from homeassistant.const import CONF_HOST, CONF_NAME, STATE_ON
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
-from . import async_get_next_ping_id
from .const import DOMAIN, ICMP_TIMEOUT, PING_PRIVS, PING_TIMEOUT
_LOGGER = logging.getLogger(__name__)
@@ -141,10 +139,10 @@ class PingBinarySensor(RestoreEntity, BinarySensorEntity):
attributes = last_state.attributes
self._ping.is_alive = True
self._ping.data = {
- "min": attributes[ATTR_ROUND_TRIP_TIME_AVG],
+ "min": attributes[ATTR_ROUND_TRIP_TIME_MIN],
"max": attributes[ATTR_ROUND_TRIP_TIME_MAX],
- "avg": attributes[ATTR_ROUND_TRIP_TIME_MDEV],
- "mdev": attributes[ATTR_ROUND_TRIP_TIME_MIN],
+ "avg": attributes[ATTR_ROUND_TRIP_TIME_AVG],
+ "mdev": attributes[ATTR_ROUND_TRIP_TIME_MDEV],
}
@@ -172,15 +170,11 @@ class PingDataICMPLib(PingData):
"""Retrieve the latest details from the host."""
_LOGGER.debug("ping address: %s", self._ip_address)
try:
- data = await self.hass.async_add_executor_job(
- partial(
- icmp_ping,
- self._ip_address,
- count=self._count,
- timeout=ICMP_TIMEOUT,
- id=async_get_next_ping_id(self.hass),
- privileged=self._privileged,
- )
+ data = await async_ping(
+ self._ip_address,
+ count=self._count,
+ timeout=ICMP_TIMEOUT,
+ privileged=self._privileged,
)
except NameLookupError:
self.is_alive = False
diff --git a/homeassistant/components/ping/const.py b/homeassistant/components/ping/const.py
index 62fca9123ba..9ca99db2419 100644
--- a/homeassistant/components/ping/const.py
+++ b/homeassistant/components/ping/const.py
@@ -15,7 +15,4 @@ PING_ATTEMPTS_COUNT = 3
DOMAIN = "ping"
PLATFORMS = ["binary_sensor"]
-PING_ID = "ping_id"
PING_PRIVS = "ping_privs"
-DEFAULT_START_ID = 129
-MAX_PING_ID = 65534
diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py
index d7d812d371d..b5acecf9314 100644
--- a/homeassistant/components/ping/device_tracker.py
+++ b/homeassistant/components/ping/device_tracker.py
@@ -1,12 +1,11 @@
"""Tracks devices by sending a ICMP echo request (ping)."""
import asyncio
from datetime import timedelta
-from functools import partial
import logging
import subprocess
import sys
-from icmplib import multiping
+from icmplib import async_multiping
import voluptuous as vol
from homeassistant import const, util
@@ -21,7 +20,6 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.async_ import gather_with_concurrency
from homeassistant.util.process import kill_subprocess
-from . import async_get_next_ping_id
from .const import DOMAIN, ICMP_TIMEOUT, PING_ATTEMPTS_COUNT, PING_PRIVS, PING_TIMEOUT
_LOGGER = logging.getLogger(__name__)
@@ -118,15 +116,11 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
async def async_update(now):
"""Update all the hosts on every interval time."""
- responses = await hass.async_add_executor_job(
- partial(
- multiping,
- ip_to_dev_id.keys(),
- count=PING_ATTEMPTS_COUNT,
- timeout=ICMP_TIMEOUT,
- privileged=privileged,
- id=async_get_next_ping_id(hass, len(ip_to_dev_id)),
- )
+ responses = await async_multiping(
+ list(ip_to_dev_id),
+ count=PING_ATTEMPTS_COUNT,
+ timeout=ICMP_TIMEOUT,
+ privileged=privileged,
)
_LOGGER.debug("Multiping responses: %s", responses)
await asyncio.gather(
diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json
index 639a30a4fa0..d25d0fc731e 100644
--- a/homeassistant/components/ping/manifest.json
+++ b/homeassistant/components/ping/manifest.json
@@ -3,7 +3,7 @@
"name": "Ping (ICMP)",
"documentation": "https://www.home-assistant.io/integrations/ping",
"codeowners": [],
- "requirements": ["icmplib==2.1.1"],
+ "requirements": ["icmplib==3.0"],
"quality_scale": "internal",
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py
index e573bf0929c..a3e0d318c03 100644
--- a/homeassistant/components/pioneer/media_player.py
+++ b/homeassistant/components/pioneer/media_player.py
@@ -112,7 +112,7 @@ class PioneerDevice(MediaPlayerEntity):
try:
try:
telnet = telnetlib.Telnet(self._host, self._port, self._timeout)
- except (ConnectionRefusedError, OSError):
+ except OSError:
_LOGGER.warning("Pioneer %s refused connection", self._name)
return
telnet.write(command.encode("ASCII") + b"\r")
@@ -125,7 +125,7 @@ class PioneerDevice(MediaPlayerEntity):
"""Get the latest details from the device."""
try:
telnet = telnetlib.Telnet(self._host, self._port, self._timeout)
- except (ConnectionRefusedError, OSError):
+ except OSError:
_LOGGER.warning("Pioneer %s refused connection", self._name)
return False
diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py
index 74536ba8393..3ecd3d26ae1 100644
--- a/homeassistant/components/pjlink/media_player.py
+++ b/homeassistant/components/pjlink/media_player.py
@@ -157,15 +157,13 @@ class PjLinkDevice(MediaPlayerEntity):
def turn_off(self):
"""Turn projector off."""
- if self._pwstate == STATE_ON:
- with self.projector() as projector:
- projector.set_power("off")
+ with self.projector() as projector:
+ projector.set_power("off")
def turn_on(self):
"""Turn projector on."""
- if self._pwstate == STATE_OFF:
- with self.projector() as projector:
- projector.set_power("on")
+ with self.projector() as projector:
+ projector.set_power("on")
def mute_volume(self, mute):
"""Mute (true) of unmute (false) media player."""
diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py
index d73b997398a..c214061c416 100644
--- a/homeassistant/components/plaato/__init__.py
+++ b/homeassistant/components/plaato/__init__.py
@@ -83,7 +83,7 @@ WEBHOOK_SCHEMA = vol.Schema(
)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Configure based on config entry."""
hass.data.setdefault(DOMAIN, {})
use_webhook = entry.data[CONF_USE_WEBHOOK]
diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json
index a95359abeaf..8d13e5d8cb0 100644
--- a/homeassistant/components/plaato/translations/de.json
+++ b/homeassistant/components/plaato/translations/de.json
@@ -6,7 +6,7 @@
"webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen."
},
"create_entry": {
- "default": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})."
+ "default": "Ihr Plaato {device_type} mit dem Namen **{device_name}** wurde erfolgreich eingerichtet!"
},
"error": {
"invalid_webhook_device": "Du hast ein Ger\u00e4t gew\u00e4hlt, das das Senden von Daten an einen Webhook nicht unterst\u00fctzt. Es ist nur f\u00fcr die Airlock verf\u00fcgbar",
@@ -19,7 +19,7 @@
"token": "F\u00fcgen Sie hier das Auth Token ein",
"use_webhook": "Webhook verwenden"
},
- "description": "Um die API abfragen zu k\u00f6nnen, wird ein `auth_token` ben\u00f6tigt, das durch folgende [diese](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) Anweisungen erhalten werden kann\n\n Ausgew\u00e4hltes Ger\u00e4t: **{Ger\u00e4tetyp}** \n\nWenn Sie lieber die eingebaute Webhook-Methode (nur Airlock) verwenden m\u00f6chten, setzen Sie bitte einen Haken und lassen Sie das Auth Token leer",
+ "description": "Um die API abfragen zu k\u00f6nnen, wird ein `auth_token` ben\u00f6tigt, das durch folgende [diese](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) Anweisungen erhalten werden kann\n\n Ausgew\u00e4hltes Ger\u00e4t: **{device_type}** \n\nWenn Sie lieber die eingebaute Webhook-Methode (nur Airlock) verwenden m\u00f6chten, setzen Sie bitte einen Haken und lassen Sie das Auth Token leer",
"title": "API-Methode ausw\u00e4hlen"
},
"user": {
@@ -28,7 +28,7 @@
"device_type": "Art des Plaato-Ger\u00e4ts"
},
"description": "M\u00f6chten Sie mit der Einrichtung beginnen?",
- "title": "Plaato Webhook einrichten"
+ "title": "Plaato Ger\u00e4te einrichten"
},
"webhook": {
"description": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url}).",
diff --git a/homeassistant/components/plaato/translations/he.json b/homeassistant/components/plaato/translations/he.json
new file mode 100644
index 00000000000..014783d7431
--- /dev/null
+++ b/homeassistant/components/plaato/translations/he.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\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.",
+ "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook."
+ },
+ "step": {
+ "user": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json
index 5d6ffd19550..3de7895a805 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.5.1",
+ "plexapi==4.6.1",
"plexauth==0.0.6",
"plexwebsocket==0.0.13"
],
diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py
index 650ed2c89b0..f9c40f1edc3 100644
--- a/homeassistant/components/plex/media_player.py
+++ b/homeassistant/components/plex/media_player.py
@@ -492,6 +492,10 @@ class PlexMediaPlayer(MediaPlayerEntity):
"Client is not currently accepting playback controls: %s", self.name
)
return
+ if not self.plex_server.has_token:
+ _LOGGER.warning(
+ "Plex integration configured without a token, playback may fail"
+ )
src = json.loads(media_id)
if isinstance(src, int):
diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py
index 4dcdda044eb..dc05a727fee 100644
--- a/homeassistant/components/plex/server.py
+++ b/homeassistant/components/plex/server.py
@@ -537,6 +537,11 @@ class PlexServer:
"""Return the plexapi PlexServer instance."""
return self._plex_server
+ @property
+ def has_token(self):
+ """Return if a token is used to connect to this Plex server."""
+ return self._token is not None
+
@property
def accounts(self):
"""Return accounts associated with the Plex server."""
diff --git a/homeassistant/components/plex/translations/he.json b/homeassistant/components/plex/translations/he.json
new file mode 100644
index 00000000000..dafda36af4c
--- /dev/null
+++ b/homeassistant/components/plex/translations/he.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "host_or_token": "\u05d7\u05d9\u05d9\u05d1 \u05dc\u05e1\u05e4\u05e7 \u05dc\u05e4\u05d7\u05d5\u05ea \u05d0\u05d7\u05d3 \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05e1\u05d9\u05de\u05d5\u05df"
+ },
+ "flow_title": "{name} ({host})",
+ "step": {
+ "manual_setup": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json
index 97587131774..9e2836202df 100644
--- a/homeassistant/components/plugwise/translations/de.json
+++ b/homeassistant/components/plugwise/translations/de.json
@@ -8,13 +8,13 @@
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
- "flow_title": "Smile: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
"flow_type": "Verbindungstyp"
},
- "description": "Details",
+ "description": "Produkt:",
"title": "Plugwise Typ"
},
"user_gateway": {
diff --git a/homeassistant/components/plugwise/translations/he.json b/homeassistant/components/plugwise/translations/he.json
new file mode 100644
index 00000000000..a89120b85ab
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/he.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "user_gateway": {
+ "data": {
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "password": "\u05de\u05d6\u05d4\u05d4 Smile",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "username": "\u05d7\u05d9\u05d9\u05da \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json
index d6d9012c21a..3d7de972fb0 100644
--- a/homeassistant/components/plugwise/translations/hu.json
+++ b/homeassistant/components/plugwise/translations/hu.json
@@ -8,7 +8,7 @@
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "Smile: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py
index ab370f53731..9f69c8579a4 100644
--- a/homeassistant/components/plum_lightpad/__init__.py
+++ b/homeassistant/components/plum_lightpad/__init__.py
@@ -51,7 +51,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Plum Lightpad from a config entry."""
_LOGGER.debug("Setting up config entry with ID = %s", entry.unique_id)
diff --git a/homeassistant/components/plum_lightpad/translations/he.json b/homeassistant/components/plum_lightpad/translations/he.json
new file mode 100644
index 00000000000..6018a28e06b
--- /dev/null
+++ b/homeassistant/components/plum_lightpad/translations/he.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05d3\u05d5\u05d0\"\u05dc"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py
index 45f58949e77..303282ead54 100644
--- a/homeassistant/components/point/__init__.py
+++ b/homeassistant/components/point/__init__.py
@@ -2,6 +2,7 @@
import asyncio
import logging
+from httpx import ConnectTimeout
from pypoint import PointSession
import voluptuous as vol
@@ -14,6 +15,7 @@ from homeassistant.const import (
CONF_WEBHOOK_ID,
)
from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
@@ -74,7 +76,7 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Point from a config entry."""
async def token_saver(token, **kwargs):
@@ -92,6 +94,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
)
try:
await session.ensure_active_token()
+ except ConnectTimeout as err:
+ _LOGGER.debug("Connection Timeout")
+ raise ConfigEntryNotReady from err
except Exception: # pylint: disable=broad-except
_LOGGER.error("Authentication Error")
return False
diff --git a/homeassistant/components/point/translations/he.json b/homeassistant/components/point/translations/he.json
new file mode 100644
index 00000000000..24decb09dd8
--- /dev/null
+++ b/homeassistant/components/point/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\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.",
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "no_flows": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3."
+ },
+ "create_entry": {
+ "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
+ },
+ "error": {
+ "no_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "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": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py
index 89e340ee95e..5ec1cb475b5 100644
--- a/homeassistant/components/poolsense/__init__.py
+++ b/homeassistant/components/poolsense/__init__.py
@@ -24,7 +24,7 @@ PLATFORMS = ["sensor", "binary_sensor"]
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up PoolSense from a config entry."""
poolsense = PoolSense(
diff --git a/homeassistant/components/poolsense/translations/he.json b/homeassistant/components/poolsense/translations/he.json
new file mode 100644
index 00000000000..f285e1ec479
--- /dev/null
+++ b/homeassistant/components/poolsense/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u05d3\u05d5\u05d0\"\u05dc",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py
index 0f63bf97986..3bc4dc9b035 100644
--- a/homeassistant/components/powerwall/__init__.py
+++ b/homeassistant/components/powerwall/__init__.py
@@ -83,7 +83,7 @@ async def _async_handle_api_changed_error(
)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Tesla Powerwall from a config entry."""
entry_id = entry.entry_id
diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py
index 982952a4830..d6c326593aa 100644
--- a/homeassistant/components/powerwall/sensor.py
+++ b/homeassistant/components/powerwall/sensor.py
@@ -3,7 +3,7 @@ import logging
from tesla_powerwall import MeterType
-from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
from homeassistant.const import DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER, PERCENTAGE
from .const import (
@@ -64,20 +64,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class PowerWallChargeSensor(PowerWallEntity, SensorEntity):
"""Representation of an Powerwall charge sensor."""
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return PERCENTAGE
-
- @property
- def name(self):
- """Device Name."""
- return "Powerwall Charge"
-
- @property
- def device_class(self):
- """Device Class."""
- return DEVICE_CLASS_BATTERY
+ _attr_name = "Powerwall Charge"
+ _attr_unit_of_measurement = PERCENTAGE
+ _attr_device_class = DEVICE_CLASS_BATTERY
@property
def unique_id(self):
@@ -93,6 +82,10 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity):
class PowerWallEnergySensor(PowerWallEntity, SensorEntity):
"""Representation of an Powerwall Energy sensor."""
+ _attr_state_class = STATE_CLASS_MEASUREMENT
+ _attr_unit_of_measurement = ENERGY_KILO_WATT
+ _attr_device_class = DEVICE_CLASS_POWER
+
def __init__(
self,
meter: MeterType,
@@ -107,26 +100,10 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity):
coordinator, site_info, status, device_type, powerwalls_serial_numbers
)
self._meter = meter
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return ENERGY_KILO_WATT
-
- @property
- def name(self):
- """Device Name."""
- return f"Powerwall {self._meter.value.title()} Now"
-
- @property
- def device_class(self):
- """Device Class."""
- return DEVICE_CLASS_POWER
-
- @property
- def unique_id(self):
- """Device Uniqueid."""
- return f"{self.base_unique_id}_{self._meter.value}_instant_power"
+ self._attr_name = f"Powerwall {self._meter.value.title()} Now"
+ self._attr_unique_id = (
+ f"{self.base_unique_id}_{self._meter.value}_instant_power"
+ )
@property
def state(self):
diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json
index c9161526373..88b0473232f 100644
--- a/homeassistant/components/powerwall/translations/de.json
+++ b/homeassistant/components/powerwall/translations/de.json
@@ -10,13 +10,14 @@
"unknown": "Unerwarteter Fehler",
"wrong_version": "Deine Powerwall verwendet eine Softwareversion, die nicht unterst\u00fctzt wird. Bitte ziehe ein Upgrade in Betracht oder melde dieses Problem, damit es behoben werden kann."
},
- "flow_title": "Tesla Powerwall ({ip_address})",
+ "flow_title": "{ip_address}",
"step": {
"user": {
"data": {
"ip_address": "IP-Adresse",
"password": "Passwort"
},
+ "description": "Das Kennwort ist in der Regel die letzten 5 Zeichen der Seriennummer des Backup Gateway und kann in der Tesla-App gefunden werden oder es sind die letzten 5 Zeichen des Kennworts, das sich in der T\u00fcr f\u00fcr Backup Gateway 2 befindet.",
"title": "Stellen Sie eine Verbindung zur Powerwall her"
}
}
diff --git a/homeassistant/components/powerwall/translations/he.json b/homeassistant/components/powerwall/translations/he.json
new file mode 100644
index 00000000000..f090e85c0cf
--- /dev/null
+++ b/homeassistant/components/powerwall/translations/he.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "flow_title": "{ip_address}",
+ "step": {
+ "user": {
+ "data": {
+ "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."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json
index a2b8af13370..9f12342595a 100644
--- a/homeassistant/components/powerwall/translations/hu.json
+++ b/homeassistant/components/powerwall/translations/hu.json
@@ -9,7 +9,7 @@
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "Tesla Powerwall ({ip_address})",
+ "flow_title": "{ip_address}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py
index e6bc68ba918..e2d44451ec2 100644
--- a/homeassistant/components/profiler/__init__.py
+++ b/homeassistant/components/profiler/__init__.py
@@ -51,7 +51,7 @@ LOG_INTERVAL_SUB = "log_interval_subscription"
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Profiler from a config entry."""
lock = asyncio.Lock()
domain_data = hass.data[DOMAIN] = {}
@@ -194,7 +194,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
return True
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
for service in SERVICES:
hass.services.async_remove(domain=DOMAIN, service=service)
diff --git a/homeassistant/components/profiler/translations/he.json b/homeassistant/components/profiler/translations/he.json
new file mode 100644
index 00000000000..08506bf3437
--- /dev/null
+++ b/homeassistant/components/profiler/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "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?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py
index 78ea16bb26c..55ed9c0241b 100644
--- a/homeassistant/components/progettihwsw/__init__.py
+++ b/homeassistant/components/progettihwsw/__init__.py
@@ -12,7 +12,7 @@ from .const import DOMAIN
PLATFORMS = ["switch", "binary_sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up ProgettiHWSW Automation from a config entry."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = ProgettiHWSWAPI(
diff --git a/homeassistant/components/progettihwsw/translations/he.json b/homeassistant/components/progettihwsw/translations/he.json
new file mode 100644
index 00000000000..67c80866be0
--- /dev/null
+++ b/homeassistant/components/progettihwsw/translations/he.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "relay_modes": {
+ "data": {
+ "relay_1": "Relay 1",
+ "relay_10": "Relay 10",
+ "relay_11": "Relay 11",
+ "relay_12": "Relay 12",
+ "relay_13": "Relay 13",
+ "relay_15": "Relay 15",
+ "relay_2": "Relay 2",
+ "relay_3": "Relay 3",
+ "relay_4": "Relay 4",
+ "relay_5": "Relay 5",
+ "relay_6": "Relay 6",
+ "relay_7": "Relay 7",
+ "relay_8": "Relay 8",
+ "relay_9": "Relay 9"
+ },
+ "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05de\u05de\u05e1\u05e8\u05d9\u05dd"
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py
index b253daf559e..c74caa745f3 100644
--- a/homeassistant/components/prometheus/__init__.py
+++ b/homeassistant/components/prometheus/__init__.py
@@ -397,7 +397,7 @@ class PrometheusMetrics:
try:
value = self.state_as_number(state)
- if unit == TEMP_FAHRENHEIT:
+ if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT:
value = fahrenheit_to_celsius(value)
_metric.labels(**self._labels(state)).set(value)
except ValueError:
diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py
index 5777bb3054c..1b0d07c69a3 100644
--- a/homeassistant/components/proxmoxve/__init__.py
+++ b/homeassistant/components/proxmoxve/__init__.py
@@ -124,6 +124,9 @@ async def async_setup(hass: HomeAssistant, config: dict):
except ConnectTimeout:
_LOGGER.warning("Connection to host %s timed out during setup", host)
continue
+ except requests.exceptions.ConnectionError:
+ _LOGGER.warning("Host %s is not reachable", host)
+ continue
hass.data[PROXMOX_CLIENTS][host] = proxmox_client
diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json
index 86f3d23d308..68c7717e16c 100644
--- a/homeassistant/components/proxy/manifest.json
+++ b/homeassistant/components/proxy/manifest.json
@@ -2,6 +2,6 @@
"domain": "proxy",
"name": "Camera Proxy",
"documentation": "https://www.home-assistant.io/integrations/proxy",
- "requirements": ["pillow==8.1.2"],
+ "requirements": ["pillow==8.2.0"],
"codeowners": []
}
diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py
index f2d284daa79..c5236f7dffe 100644
--- a/homeassistant/components/ps4/const.py
+++ b/homeassistant/components/ps4/const.py
@@ -54,7 +54,7 @@ COUNTRYCODE_NAMES = {
"LU": "Luxembourg",
"MT": "Malta",
"MX": "Mexico",
- "MY": "Maylasia",
+ "MY": "Maylasia", # spelling error compatibility with pyps4_2ndscreen.media_art.COUNTRIES
"NI": "Nicaragua",
"NL": "Nederland",
"NO": "Norway",
diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json
index d5aa867f1db..1a20740dfb1 100644
--- a/homeassistant/components/ps4/translations/de.json
+++ b/homeassistant/components/ps4/translations/de.json
@@ -25,7 +25,7 @@
"name": "Name",
"region": "Region"
},
- "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein.",
+ "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein. Weitere Informationen finden Sie in der [Dokumentation](https://www.home-assistant.io/components/ps4/).",
"title": "PlayStation 4"
},
"mode": {
diff --git a/homeassistant/components/ps4/translations/he.json b/homeassistant/components/ps4/translations/he.json
index ec0807982cc..837dd13b925 100644
--- a/homeassistant/components/ps4/translations/he.json
+++ b/homeassistant/components/ps4/translations/he.json
@@ -1,14 +1,19 @@
{
"config": {
"abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
"no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9 \u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4 \u05d1\u05e8\u05e9\u05ea."
},
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
"step": {
"creds": {
"title": "\u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4"
},
"link": {
"data": {
+ "code": "\u05e7\u05d5\u05d3 PIN",
"ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4 - IP",
"name": "\u05e9\u05dd",
"region": "\u05d0\u05d9\u05d6\u05d5\u05e8"
diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py
index 2ab8f387bda..3e98274c696 100644
--- a/homeassistant/components/pvpc_hourly_pricing/__init__.py
+++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py
@@ -1,17 +1,38 @@
"""The pvpc_hourly_pricing integration to collect Spain official electric prices."""
+import logging
+
+from aiopvpc import DEFAULT_POWER_KW, TARIFFS
import voluptuous as vol
-from homeassistant import config_entries
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_NAME
-from homeassistant.core import HomeAssistant
+from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity_registry import (
+ EntityRegistry,
+ async_get,
+ async_migrate_entries,
+)
-from .const import ATTR_TARIFF, DEFAULT_NAME, DEFAULT_TARIFF, DOMAIN, PLATFORMS, TARIFFS
+from .const import (
+ ATTR_POWER,
+ ATTR_POWER_P3,
+ ATTR_TARIFF,
+ DEFAULT_NAME,
+ DOMAIN,
+ PLATFORMS,
+)
+_LOGGER = logging.getLogger(__name__)
+_DEFAULT_TARIFF = TARIFFS[0]
+VALID_POWER = vol.All(vol.Coerce(float), vol.Range(min=1.0, max=15.0))
+VALID_TARIFF = vol.In(TARIFFS)
UI_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
- vol.Required(ATTR_TARIFF, default=DEFAULT_TARIFF): vol.In(TARIFFS),
+ vol.Required(ATTR_TARIFF, default=_DEFAULT_TARIFF): VALID_TARIFF,
+ vol.Required(ATTR_POWER, default=DEFAULT_POWER_KW): VALID_POWER,
+ vol.Required(ATTR_POWER_P3, default=DEFAULT_POWER_KW): VALID_POWER,
}
)
CONFIG_SCHEMA = vol.Schema(
@@ -20,35 +41,81 @@ CONFIG_SCHEMA = vol.Schema(
)
-async def async_setup(hass: HomeAssistant, config: dict):
- """
- Set up the electricity price sensor from configuration.yaml.
-
- ```yaml
- pvpc_hourly_pricing:
- - name: PVPC manual ve
- tariff: electric_car
- - name: PVPC manual nocturna
- tariff: discrimination
- timeout: 3
- ```
- """
+async def async_setup(hass: HomeAssistant, config: dict) -> bool:
+ """Set up the electricity price sensor from configuration.yaml."""
for conf in config.get(DOMAIN, []):
hass.async_create_task(
hass.config_entries.flow.async_init(
- DOMAIN, data=conf, context={"source": config_entries.SOURCE_IMPORT}
+ DOMAIN, data=conf, context={"source": SOURCE_IMPORT}
)
)
return True
-async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up pvpc hourly pricing from a config entry."""
+ if len(entry.data) == 2:
+ defaults = {
+ ATTR_TARIFF: _DEFAULT_TARIFF,
+ ATTR_POWER: DEFAULT_POWER_KW,
+ ATTR_POWER_P3: DEFAULT_POWER_KW,
+ }
+ data = {**entry.data, **defaults}
+ hass.config_entries.async_update_entry(
+ entry, unique_id=_DEFAULT_TARIFF, data=data, options=defaults
+ )
+
+ @callback
+ def update_unique_id(reg_entry):
+ """Change unique id for sensor entity, pointing to new tariff."""
+ return {"new_unique_id": _DEFAULT_TARIFF}
+
+ try:
+ await async_migrate_entries(hass, entry.entry_id, update_unique_id)
+ _LOGGER.warning(
+ "Migrating PVPC sensor from old tariff '%s' to new '%s'. "
+ "Configure the integration to set your contracted power, "
+ "and select prices for Ceuta/Melilla, "
+ "if that is your case",
+ entry.data[ATTR_TARIFF],
+ _DEFAULT_TARIFF,
+ )
+ except ValueError:
+ # there were multiple sensors (with different old tariffs, up to 3),
+ # so we leave just one and remove the others
+ ent_reg: EntityRegistry = async_get(hass)
+ for entity_id, reg_entry in ent_reg.entities.items():
+ if reg_entry.config_entry_id == entry.entry_id:
+ ent_reg.async_remove(entity_id)
+ _LOGGER.warning(
+ "Old PVPC Sensor %s is removed "
+ "(another one already exists, using the same tariff)",
+ entity_id,
+ )
+ break
+
+ await hass.config_entries.async_remove(entry.entry_id)
+ return False
+
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+ entry.async_on_unload(entry.add_update_listener(async_update_options))
return True
-async def async_unload_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry):
+async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
+ """Handle options update."""
+ if any(
+ entry.data.get(attrib) != entry.options.get(attrib)
+ for attrib in (ATTR_TARIFF, ATTR_POWER, ATTR_POWER_P3)
+ ):
+ # update entry replacing data with new options
+ hass.config_entries.async_update_entry(
+ entry, data={**entry.data, **entry.options}
+ )
+ 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/pvpc_hourly_pricing/config_flow.py b/homeassistant/components/pvpc_hourly_pricing/config_flow.py
index 971a13acc2f..76694d570b5 100644
--- a/homeassistant/components/pvpc_hourly_pricing/config_flow.py
+++ b/homeassistant/components/pvpc_hourly_pricing/config_flow.py
@@ -1,17 +1,24 @@
"""Config flow for pvpc_hourly_pricing."""
+import voluptuous as vol
+
from homeassistant import config_entries
+from homeassistant.core import callback
-from . import CONF_NAME, UI_CONFIG_SCHEMA
-from .const import ATTR_TARIFF, DOMAIN
-
-_DOMAIN_NAME = DOMAIN
+from . import CONF_NAME, UI_CONFIG_SCHEMA, VALID_POWER, VALID_TARIFF
+from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF, DOMAIN
-class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=_DOMAIN_NAME):
- """Handle a config flow for `pvpc_hourly_pricing` to select the tariff."""
+class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle config flow for `pvpc_hourly_pricing`."""
VERSION = 1
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return PVPCOptionsFlowHandler(config_entry)
+
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if user_input is not None:
@@ -24,3 +31,35 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=_DOMAIN_NAME):
async def async_step_import(self, import_info):
"""Handle import from config file."""
return await self.async_step_user(import_info)
+
+
+class PVPCOptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle PVPC options."""
+
+ 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."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ # Fill options with entry data
+ tariff = self.config_entry.options.get(
+ ATTR_TARIFF, self.config_entry.data[ATTR_TARIFF]
+ )
+ power = self.config_entry.options.get(
+ ATTR_POWER, self.config_entry.data[ATTR_POWER]
+ )
+ power_valley = self.config_entry.options.get(
+ ATTR_POWER_P3, self.config_entry.data[ATTR_POWER_P3]
+ )
+ schema = vol.Schema(
+ {
+ vol.Required(ATTR_TARIFF, default=tariff): VALID_TARIFF,
+ vol.Required(ATTR_POWER, default=power): VALID_POWER,
+ vol.Required(ATTR_POWER_P3, default=power_valley): VALID_POWER,
+ }
+ )
+ return self.async_show_form(step_id="init", data_schema=schema)
diff --git a/homeassistant/components/pvpc_hourly_pricing/const.py b/homeassistant/components/pvpc_hourly_pricing/const.py
index 9e11bc57d6d..ad97124c330 100644
--- a/homeassistant/components/pvpc_hourly_pricing/const.py
+++ b/homeassistant/components/pvpc_hourly_pricing/const.py
@@ -1,8 +1,7 @@
"""Constant values for pvpc_hourly_pricing."""
-from aiopvpc import TARIFFS
-
DOMAIN = "pvpc_hourly_pricing"
PLATFORMS = ["sensor"]
+ATTR_POWER = "power"
+ATTR_POWER_P3 = "power_p3"
ATTR_TARIFF = "tariff"
DEFAULT_NAME = "PVPC"
-DEFAULT_TARIFF = TARIFFS[1]
diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json
index bbbe18350c8..612376a7931 100644
--- a/homeassistant/components/pvpc_hourly_pricing/manifest.json
+++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json
@@ -3,7 +3,7 @@
"name": "Spain electricity hourly pricing (PVPC)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing",
- "requirements": ["aiopvpc==2.1.2"],
+ "requirements": ["aiopvpc==2.2.0"],
"codeowners": ["@azogue"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"
diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py
index 5fe65e3dc65..75881f93f0a 100644
--- a/homeassistant/components/pvpc_hourly_pricing/sensor.py
+++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py
@@ -3,19 +3,21 @@ from __future__ import annotations
import logging
from random import randint
+from typing import Any
from aiopvpc import PVPCData
-from homeassistant import config_entries
from homeassistant.components.sensor import SensorEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CURRENCY_EURO, ENERGY_KILO_WATT_HOUR
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later, async_track_time_change
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.dt as dt_util
-from .const import ATTR_TARIFF
+from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF
_LOGGER = logging.getLogger(__name__)
@@ -27,15 +29,18 @@ _DEFAULT_TIMEOUT = 10
async def async_setup_entry(
- hass: HomeAssistant, config_entry: config_entries.ConfigEntry, async_add_entities
-):
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
"""Set up the electricity price sensor from config_entry."""
name = config_entry.data[CONF_NAME]
pvpc_data_handler = PVPCData(
tariff=config_entry.data[ATTR_TARIFF],
+ power=config_entry.data[ATTR_POWER],
+ power_valley=config_entry.data[ATTR_POWER_P3],
local_timezone=hass.config.time_zone,
websession=async_get_clientsession(hass),
- logger=_LOGGER,
timeout=_DEFAULT_TIMEOUT,
)
async_add_entities(
@@ -57,15 +62,7 @@ class ElecPriceSensor(RestoreEntity, SensorEntity):
self._pvpc_data = pvpc_data_handler
self._num_retries = 0
- self._hourly_tracker = None
- self._price_tracker = None
-
- async def async_will_remove_from_hass(self) -> None:
- """Cancel listeners for sensor updates."""
- self._hourly_tracker()
- self._price_tracker()
-
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
@@ -73,14 +70,18 @@ class ElecPriceSensor(RestoreEntity, SensorEntity):
self._pvpc_data.state = state.state
# Update 'state' value in hour changes
- self._hourly_tracker = async_track_time_change(
- self.hass, self.update_current_price, second=[0], minute=[0]
+ self.async_on_remove(
+ async_track_time_change(
+ self.hass, self.update_current_price, second=[0], minute=[0]
+ )
)
# Update prices at random time, 2 times/hour (don't want to upset API)
random_minute = randint(1, 29)
mins_update = [random_minute, random_minute + 30]
- self._price_tracker = async_track_time_change(
- self.hass, self.async_update_prices, second=[0], minute=mins_update
+ self.async_on_remove(
+ async_track_time_change(
+ self.hass, self.async_update_prices, second=[0], minute=mins_update
+ )
)
_LOGGER.debug(
"Setup of price sensor %s (%s) with tariff '%s', "
@@ -90,8 +91,9 @@ class ElecPriceSensor(RestoreEntity, SensorEntity):
self._pvpc_data.tariff,
mins_update,
)
- await self.async_update_prices(dt_util.utcnow())
- self.update_current_price(dt_util.utcnow())
+ now = dt_util.utcnow()
+ await self.async_update_prices(now)
+ self.update_current_price(now)
@property
def unique_id(self) -> str | None:
@@ -99,12 +101,12 @@ class ElecPriceSensor(RestoreEntity, SensorEntity):
return self._unique_id
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
- def state(self):
+ def state(self) -> float:
"""Return the state of the sensor."""
return self._pvpc_data.state
@@ -114,7 +116,7 @@ class ElecPriceSensor(RestoreEntity, SensorEntity):
return self._pvpc_data.state_available
@property
- def extra_state_attributes(self):
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return self._pvpc_data.attributes
diff --git a/homeassistant/components/pvpc_hourly_pricing/strings.json b/homeassistant/components/pvpc_hourly_pricing/strings.json
index a1536d2186f..89da917c8ea 100644
--- a/homeassistant/components/pvpc_hourly_pricing/strings.json
+++ b/homeassistant/components/pvpc_hourly_pricing/strings.json
@@ -2,16 +2,31 @@
"config": {
"step": {
"user": {
- "title": "Tariff selection",
- "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/).\n\nSelect the contracted rate based on the number of billing periods per day:\n- 1 period: normal\n- 2 periods: discrimination (nightly rate)\n- 3 periods: electric car (nightly rate of 3 periods)",
+ "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": "Contracted tariff (1, 2, or 3 periods)"
+ "tariff": "Applicable tariff by geographic zone",
+ "power": "Contracted power (kW)",
+ "power_p3": "Contracted power for valley period P3 (kW)"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
+ },
+ "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)",
+ "power_p3": "Contracted power for valley period P3 (kW)"
+ }
+ }
+ }
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json
index bc5f3a59428..ef5e00e25c3 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json
@@ -7,10 +7,25 @@
"user": {
"data": {
"name": "Nom del sensor",
- "tariff": "Tarifa contractada (1, 2 o 3 per\u00edodes)"
+ "power": "Pot\u00e8ncia contractada (kW)",
+ "power_p3": "Pot\u00e8ncia contractada del per\u00edode vall P3 (kW)",
+ "tariff": "Tarifa aplicable per zona geogr\u00e0fica"
},
- "description": "Aquest sensor utilitza l'API oficial de la xarxa el\u00e8ctrica espanyola (REE) per obtenir els [preus per hora de l'electricitat (PVPC)](https://www.esios.ree.es/es/pvpc) a Espanya.\nPer a m\u00e9s informaci\u00f3, consulta la [documentaci\u00f3 de la integraci\u00f3](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSelecciona la tarifa contractada, cadascuna t\u00e9 un nombre determinat de per\u00edodes: \n - 1 per\u00edode: normal (sense discriminaci\u00f3)\n - 2 per\u00edodes: discriminaci\u00f3 (tarifa nocturna) \n - 3 per\u00edodes: cotxe el\u00e8ctric (tarifa nocturna de 3 per\u00edodes)",
- "title": "Selecci\u00f3 de tarifa"
+ "description": "Aquest sensor utilitza l'API oficial per obtenir els [preus per hora de l'electricitat (PVPC)](https://www.esios.ree.es/es/pvpc) a Espanya.\nPer a m\u00e9s informaci\u00f3, consulta la [documentaci\u00f3 de la integraci\u00f3](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "Configuraci\u00f3 del sensor"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "power": "Pot\u00e8ncia contractada (kW)",
+ "power_p3": "Pot\u00e8ncia contractada del per\u00edode vall P3 (kW)",
+ "tariff": "Tarifa aplicable per zona geogr\u00e0fica"
+ },
+ "description": "Aquest sensor utilitza l'API oficial per obtenir els [preus per hora de l'electricitat (PVPC)](https://www.esios.ree.es/es/pvpc) a Espanya.\nPer a m\u00e9s informaci\u00f3, consulta la [documentaci\u00f3 de la integraci\u00f3](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "Configuraci\u00f3 del sensor"
}
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/de.json b/homeassistant/components/pvpc_hourly_pricing/translations/de.json
index 1b5c4d37658..04f5ddb8fba 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/de.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/de.json
@@ -7,10 +7,25 @@
"user": {
"data": {
"name": "Sensorname",
- "tariff": "Vertragstarif (1, 2 oder 3 Perioden)"
+ "power": "Vertraglich vereinbarte Leistung (kW)",
+ "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)",
+ "tariff": "Geltender Tarif nach geografischer Zone"
},
- "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. \nWeitere Informationen finden Sie in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nW\u00e4hlen Sie den vertraglich vereinbarten Tarif basierend auf der Anzahl der Abrechnungsperioden pro Tag aus: \n - 1 Periode: Normal \n - 2 Perioden: Diskriminierung (Nachttarif) \n - 3 Perioden: Elektroauto (Nachttarif von 3 Perioden)",
- "title": "Tarifauswahl"
+ "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. Weitere Informationen finden Sie in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). ",
+ "title": "Sensoreinrichtung"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "power": "Vertraglich vereinbarte Leistung (kW)",
+ "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)",
+ "tariff": "Geltender Tarif nach geografischer Zone"
+ },
+ "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)](https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten.\nEine genauere Erkl\u00e4rung finden Sie in den [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "Sensoreinrichtung"
}
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/en.json b/homeassistant/components/pvpc_hourly_pricing/translations/en.json
index 02acb46eeb6..38f45d36ab6 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/en.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/en.json
@@ -7,10 +7,25 @@
"user": {
"data": {
"name": "Sensor Name",
- "tariff": "Contracted tariff (1, 2, or 3 periods)"
+ "power": "Contracted power (kW)",
+ "power_p3": "Contracted power for valley period P3 (kW)",
+ "tariff": "Applicable tariff by geographic zone"
},
- "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/).\n\nSelect the contracted rate based on the number of billing periods per day:\n- 1 period: normal\n- 2 periods: discrimination (nightly rate)\n- 3 periods: electric car (nightly rate of 3 periods)",
- "title": "Tariff selection"
+ "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/).",
+ "title": "Sensor setup"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "power": "Contracted power (kW)",
+ "power_p3": "Contracted power for valley period P3 (kW)",
+ "tariff": "Applicable tariff by geographic zone"
+ },
+ "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/).",
+ "title": "Sensor setup"
}
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/es.json b/homeassistant/components/pvpc_hourly_pricing/translations/es.json
index abc493b0790..59c6f6de174 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/es.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/es.json
@@ -7,11 +7,26 @@
"user": {
"data": {
"name": "Nombre del sensor",
+ "power": "Potencia contratada (kW)",
+ "power_p3": "Potencia contratada para el per\u00edodo valle P3 (kW)",
"tariff": "Tarifa contratada (1, 2 o 3 per\u00edodos)"
},
"description": "Este sensor utiliza la API oficial para obtener [el precio horario de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\u00f1a.\nPara obtener una explicaci\u00f3n m\u00e1s precisa, visita los [documentos de la integraci\u00f3n](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelecciona la tarifa contratada en funci\u00f3n del n\u00famero de per\u00edodos de facturaci\u00f3n por d\u00eda:\n- 1 per\u00edodo: normal\n- 2 per\u00edodos: discriminaci\u00f3n (tarifa nocturna)\n- 3 per\u00edodos: coche el\u00e9ctrico (tarifa nocturna de 3 per\u00edodos)",
"title": "Selecci\u00f3n de tarifa"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "power": "Potencia contratada (kW)",
+ "power_p3": "Potencia contratada para el per\u00edodo valle P3 (kW)",
+ "tariff": "Tarifa aplicable por zona geogr\u00e1fica"
+ },
+ "description": "Este sensor utiliza la API oficial para obtener el [precio horario de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\u00f1a.\nPara una explicaci\u00f3n m\u00e1s precisa visite los [documentos de integraci\u00f3n](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "Configuraci\u00f3n del sensor"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/et.json b/homeassistant/components/pvpc_hourly_pricing/translations/et.json
index e80e5ef68cd..5db4f9ec04b 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/et.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/et.json
@@ -7,10 +7,25 @@
"user": {
"data": {
"name": "Anduri nimi",
- "tariff": "Lepinguline tariif (1, 2 v\u00f5i 3 perioodi)"
+ "power": "Lepinguj\u00e4rgne v\u00f5imsus (kW)",
+ "power_p3": "Lepinguj\u00e4rgne v\u00f5imsus soodusperioodil P3 (kW)",
+ "tariff": "Kohaldatav tariif geograafilise tsooni j\u00e4rgi"
},
- "description": "See andur kasutab ametlikku API-d, et saada Hispaania [tunni hinnakujundus (PVPC)] (https://www.esios.ree.es/es/pvpc) elektri tunnihind.\n T\u00e4psema selgituse saamiseks k\u00fclasta [integratsioonidokumente] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) \n\n Vali lepinguline m\u00e4\u00e4r l\u00e4htudes arveldusperioodide arvust p\u00e4evas:\n - 1 periood: normaalne\n - 2 perioodi: v\u00e4hendatud (\u00f6\u00f6hind)\n - 3 perioodi: elektriauto (\u00f6\u00f6 hind 3 perioodi)",
- "title": "Tariifivalik"
+ "description": "See andur kasutab ametlikku API-d, et saada [elektri tunnihinda (PVPC)](https://www.esios.ree.es/es/pvpc) Hispaanias.\nT\u00e4psema selgituse saamiseks k\u00fclasta [integratsioonidokumente](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "Anduri seadistamine"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "power": "Lepinguj\u00e4rgne v\u00f5imsus (kW)",
+ "power_p3": "Lepinguj\u00e4rgne v\u00f5imsus soodusperioodil P3 (kW)",
+ "tariff": "Kohaldatav tariif geograafilise tsooni j\u00e4rgi"
+ },
+ "description": "See andur kasutab ametlikku API-d, et saada [elektri tunnihinda (PVPC)](https://www.esios.ree.es/es/pvpc) Hispaanias.\nT\u00e4psema selgituse saamiseks k\u00fclasta [integratsioonidokumente](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "Anduri seadistamine"
}
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/he.json b/homeassistant/components/pvpc_hourly_pricing/translations/he.json
new file mode 100644
index 00000000000..48a6eeeea33
--- /dev/null
+++ b/homeassistant/components/pvpc_hourly_pricing/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/pvpc_hourly_pricing/translations/hu.json b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json
index f5301e874ea..17bca647c18 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json
@@ -3,5 +3,12 @@
"abort": {
"already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "\u00c9rz\u00e9kel\u0151 be\u00e1ll\u00edt\u00e1sa"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/it.json b/homeassistant/components/pvpc_hourly_pricing/translations/it.json
index e36fc746883..79e6e627a7e 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/it.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/it.json
@@ -7,10 +7,25 @@
"user": {
"data": {
"name": "Nome del sensore",
- "tariff": "Tariffa contrattuale (1, 2 o 3 periodi)"
+ "power": "Potenza contrattuale (kW)",
+ "power_p3": "Potenza contrattuale per il periodo di valle P3 (kW)",
+ "tariff": "Tariffa applicabile per zona geografica"
},
- "description": "Questo sensore utilizza l'API ufficiale per ottenere [prezzi orari dell'elettricit\u00e0 (PVPC)](https://www.esios.ree.es/es/pvpc) in Spagna.\nPer una spiegazione pi\u00f9 precisa, visitare la [documentazione di integrazione](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelezionare la tariffa contrattuale in base al numero di periodi di fatturazione al giorno:\n- 1 periodo: normale\n- 2 periodi: discriminazione (tariffa notturna)\n- 3 periodi: auto elettrica (tariffa notturna di 3 periodi)",
- "title": "Selezione della tariffa"
+ "description": "Questo sensore utilizza l'API ufficiale per ottenere il [prezzo orario dell'elettricit\u00e0 (PVPC)](https://www.esios.ree.es/es/pvpc) in Spagna.\nPer una spiegazione pi\u00f9 precisa, visita i [documenti di integrazione](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "Configurazione del sensore"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "power": "Potenza contrattuale (kW)",
+ "power_p3": "Potenza contrattuale per il periodo di valle P3 (kW)",
+ "tariff": "Tariffa applicabile per zona geografica"
+ },
+ "description": "Questo sensore utilizza l'API ufficiale per ottenere il [prezzo orario dell'elettricit\u00e0 (PVPC)](https://www.esios.ree.es/es/pvpc) in Spagna.\nPer una spiegazione pi\u00f9 precisa, visita i [documenti di integrazione](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "Configurazione del sensore"
}
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json
index 5048ed498df..f74662b06da 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json
@@ -7,11 +7,26 @@
"user": {
"data": {
"name": "Sensornaam",
+ "power": "Gecontracteerd vermogen (kW)",
+ "power_p3": "Gecontracteerd vermogen voor dalperiode P3 (kW)",
"tariff": "Gecontracteerd tarief (1, 2 of 3 periodes)"
},
"description": "Deze sensor gebruikt de offici\u00eble API om [uurtarief voor elektriciteit (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanje te krijgen. \n Bezoek voor een meer precieze uitleg de [integratiedocumenten] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Selecteer het gecontracteerde tarief op basis van het aantal factureringsperioden per dag: \n - 1 periode: normaal \n - 2 periodes: discriminatie (nachttarief) \n - 3 periodes: elektrische auto (nachttarief van 3 periodes)",
"title": "Tariefselectie"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "power": "Gecontracteerd vermogen (kW)",
+ "power_p3": "Gecontracteerd vermogen voor dalperiode P3 (kW)",
+ "tariff": "Toepasselijk tarief per geografische zone"
+ },
+ "description": "Deze sensor maakt gebruik van offici\u00eble API om [uurprijzen van elektriciteit (PVPC)](https://www.esios.ree.es/es/pvpc) in Spanje te krijgen.\nGa voor een preciezere uitleg naar de [integratiedocumenten](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "Sensor setup"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/no.json b/homeassistant/components/pvpc_hourly_pricing/translations/no.json
index 7eec443fcaa..7429626674e 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/no.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/no.json
@@ -7,10 +7,25 @@
"user": {
"data": {
"name": "Sensornavn",
- "tariff": "Avtaletariff (1, 2 eller 3 perioder)"
+ "power": "Kontrahert effekt (kW)",
+ "power_p3": "Kontraktstr\u00f8m for dalperiode P3 (kW)",
+ "tariff": "Gjeldende tariff etter geografisk sone"
},
- "description": "Denne sensoren bruker offisiell API for \u00e5 f\u00e5 [timeprising av elektrisitet (PVPC)](https://www.esios.ree.es/es/pvpc) i Spania.\nFor mer presis forklaring, bes\u00f8k [integrasjonsdokumenter](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nVelg den avtalte satsen basert p\u00e5 antall faktureringsperioder per dag:\n- 1 periode: normal\n- 2 perioder: diskriminering (nattlig rate)\n- 3 perioder: elbil (per natt rate p\u00e5 3 perioder)",
- "title": "Tariffvalg"
+ "description": "Denne sensoren bruker offisiell API for \u00e5 f\u00e5 [timeprisering av elektrisitet (PVPC)] (https://www.esios.ree.es/es/pvpc) i Spania.\n For mer presis forklaring bes\u00f8k [integrasjonsdokumentene] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "Oppsett av sensor"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "power": "Kontrahert effekt (kW)",
+ "power_p3": "Kontrahert kraft for dalperioden P3 (kW)",
+ "tariff": "Gjeldende tariff etter geografisk sone"
+ },
+ "description": "Denne sensoren bruker offisiell API for \u00e5 f\u00e5 [timeprisering av elektrisitet (PVPC)] (https://www.esios.ree.es/es/pvpc) i Spania.\n For mer presis forklaring bes\u00f8k [integrasjonsdokumentene] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "Oppsett av sensor"
}
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json
index f6606c570aa..d689fd9383a 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json
@@ -7,10 +7,25 @@
"user": {
"data": {
"name": "Nazwa sensora",
- "tariff": "Zakontraktowana taryfa (1, 2 lub 3 okresy)"
+ "power": "Moc zakontraktowana (kW)",
+ "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)",
- "title": "Wyb\u00f3r taryfy"
+ "title": "Konfiguracja sensora"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "power": "Moc zakontraktowana (kW)",
+ "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)",
+ "title": "Konfiguracja sensora"
}
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ru.json b/homeassistant/components/pvpc_hourly_pricing/translations/ru.json
index e48e5222f3c..e68bc7e6289 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/ru.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/ru.json
@@ -7,10 +7,25 @@
"user": {
"data": {
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
- "tariff": "\u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043d\u044b\u0439 \u0442\u0430\u0440\u0438\u0444 (1, 2 \u0438\u043b\u0438 3 \u043f\u0435\u0440\u0438\u043e\u0434\u0430)"
+ "power": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c (\u043a\u0412\u0442)",
+ "power_p3": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u043d\u0430 \u043f\u0435\u0440\u0438\u043e\u0434 P3 (\u043a\u0412\u0442)",
+ "tariff": "\u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u0439 \u0442\u0430\u0440\u0438\u0444 \u043f\u043e \u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0437\u043e\u043d\u0435"
},
- "description": "\u042d\u0442\u043e\u0442 \u0441\u0435\u043d\u0441\u043e\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 API \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f [\u043f\u043e\u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0446\u0435\u043d\u044b \u0437\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u044d\u043d\u0435\u0440\u0433\u0438\u044e (PVPC)](https://www.esios.ree.es/es/pvpc) \u0432 \u0418\u0441\u043f\u0430\u043d\u0438\u0438.\n\u0414\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\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\n\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0430\u0440\u0438\u0444, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043d\u0430 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0435 \u0440\u0430\u0441\u0447\u0435\u0442\u043d\u044b\u0445 \u043f\u0435\u0440\u0438\u043e\u0434\u043e\u0432 \u0432 \u0434\u0435\u043d\u044c:\n- 1 \u043f\u0435\u0440\u0438\u043e\u0434: normal\n- 2 \u043f\u0435\u0440\u0438\u043e\u0434\u0430: discrimination (nightly rate)\n- 3 \u043f\u0435\u0440\u0438\u043e\u0434\u0430: electric car (nightly rate of 3 periods)",
- "title": "\u0412\u044b\u0431\u043e\u0440 \u0442\u0430\u0440\u0438\u0444\u0430"
+ "description": "\u042d\u0442\u043e\u0442 \u0441\u0435\u043d\u0441\u043e\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 API \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f [\u043f\u043e\u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0446\u0435\u043d\u044b \u0437\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u044d\u043d\u0435\u0440\u0433\u0438\u044e (PVPC)](https://www.esios.ree.es/es/pvpc) \u0432 \u0418\u0441\u043f\u0430\u043d\u0438\u0438.\n\u0414\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\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "power": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c (\u043a\u0412\u0442)",
+ "power_p3": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u043d\u0430 \u043f\u0435\u0440\u0438\u043e\u0434 P3 (\u043a\u0412\u0442)",
+ "tariff": "\u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u0439 \u0442\u0430\u0440\u0438\u0444 \u043f\u043e \u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0437\u043e\u043d\u0435"
+ },
+ "description": "\u042d\u0442\u043e\u0442 \u0441\u0435\u043d\u0441\u043e\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 API \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f [\u043f\u043e\u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0446\u0435\u043d\u044b \u0437\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u044d\u043d\u0435\u0440\u0433\u0438\u044e (PVPC)](https://www.esios.ree.es/es/pvpc) \u0432 \u0418\u0441\u043f\u0430\u043d\u0438\u0438.\n\u0414\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\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430"
}
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json b/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json
index 15b6539281c..1cadebbc6be 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json
@@ -7,10 +7,25 @@
"user": {
"data": {
"name": "\u50b3\u611f\u5668\u540d\u7a31",
- "tariff": "\u5408\u7d04\u8cbb\u7387\uff081\u30012 \u6216 3 \u9031\u671f\uff09"
+ "power": "\u5408\u7d04\u529f\u7387\uff08kW\uff09",
+ "power_p3": "\u4f4e\u5cf0\u671f P3 \u5408\u7d04\u529f\u7387\uff08kW\uff09",
+ "tariff": "\u5206\u5340\u9069\u7528\u8cbb\u7387"
},
- "description": "\u6b64\u50b3\u611f\u5668\u4f7f\u7528\u4e86\u975e\u5b98\u65b9 API \u4ee5\u53d6\u5f97\u897f\u73ed\u7259 [\u8a08\u6642\u96fb\u50f9\uff08PVPC\uff09](https://www.esios.ree.es/es/pvpc)\u3002\n\u95dc\u65bc\u66f4\u8a73\u7d30\u7684\u8aaa\u660e\uff0c\u8acb\u53c3\u95b1 [\u6574\u5408\u6587\u4ef6](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\u3002\n\n\u57fa\u65bc\u6bcf\u5929\u7684\u5e33\u55ae\u9031\u671f\u9078\u64c7\u5408\u7d04\u8cbb\u7387\uff1a\n- 1 \u9031\u671f\uff1a\u4e00\u822c\n- 2 \u9031\u671f\uff1a\u5dee\u5225\u8cbb\u7387\uff08\u591c\u9593\u8cbb\u7387\uff09\n- 3 \u9031\u671f\uff1a\u96fb\u52d5\u8eca\uff08\u591c\u9593\u8cbb\u7387 3 \u9031\u671f\uff09",
- "title": "\u8cbb\u7387\u9078\u64c7"
+ "description": "\u6b64\u50b3\u611f\u5668\u4f7f\u7528\u4e86\u975e\u5b98\u65b9 API \u4ee5\u53d6\u5f97\u897f\u73ed\u7259 [\u8a08\u6642\u96fb\u50f9\uff08PVPC\uff09](https://www.esios.ree.es/es/pvpc)\u3002\n\u95dc\u65bc\u66f4\u8a73\u7d30\u7684\u8aaa\u660e\uff0c\u8acb\u53c3\u95b1 [\u6574\u5408\u6587\u4ef6](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\u3002",
+ "title": "\u611f\u61c9\u5668\u8a2d\u5b9a"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "power": "\u5408\u7d04\u529f\u7387\uff08kW\uff09",
+ "power_p3": "\u4f4e\u5cf0\u671f P3 \u5408\u7d04\u529f\u7387\uff08kW\uff09",
+ "tariff": "\u5206\u5340\u9069\u7528\u8cbb\u7387"
+ },
+ "description": "\u6b64\u50b3\u611f\u5668\u4f7f\u7528\u4e86\u975e\u5b98\u65b9 API \u4ee5\u53d6\u5f97\u897f\u73ed\u7259 [\u8a08\u6642\u96fb\u50f9\uff08PVPC\uff09](https://www.esios.ree.es/es/pvpc)\u3002\n\u95dc\u65bc\u66f4\u8a73\u7d30\u7684\u8aaa\u660e\uff0c\u8acb\u53c3\u95b1 [\u6574\u5408\u6587\u4ef6](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\u3002",
+ "title": "\u611f\u61c9\u5668\u8a2d\u5b9a"
}
}
}
diff --git a/homeassistant/components/qld_bushfire/manifest.json b/homeassistant/components/qld_bushfire/manifest.json
index aeddc8cbeb0..5b3de2cf62b 100644
--- a/homeassistant/components/qld_bushfire/manifest.json
+++ b/homeassistant/components/qld_bushfire/manifest.json
@@ -2,7 +2,7 @@
"domain": "qld_bushfire",
"name": "Queensland Bushfire Alert",
"documentation": "https://www.home-assistant.io/integrations/qld_bushfire",
- "requirements": ["georss_qld_bushfire_alert_client==0.3"],
+ "requirements": ["georss_qld_bushfire_alert_client==0.5"],
"codeowners": ["@exxamalte"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json
index 18bf2d7db6d..a414e197fd6 100644
--- a/homeassistant/components/qrcode/manifest.json
+++ b/homeassistant/components/qrcode/manifest.json
@@ -2,7 +2,7 @@
"domain": "qrcode",
"name": "QR Code",
"documentation": "https://www.home-assistant.io/integrations/qrcode",
- "requirements": ["pillow==8.1.2", "pyzbar==0.1.7"],
+ "requirements": ["pillow==8.2.0", "pyzbar==0.1.7"],
"codeowners": [],
"iot_class": "calculated"
}
diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py
index 3f75537cc8d..3e8e26e2a13 100644
--- a/homeassistant/components/rachio/__init__.py
+++ b/homeassistant/components/rachio/__init__.py
@@ -39,7 +39,7 @@ async def async_remove_entry(hass, entry):
await hass.components.cloud.async_delete_cloudhook(entry.data[CONF_WEBHOOK_ID])
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Rachio config entry."""
config = entry.data
diff --git a/homeassistant/components/rachio/translations/he.json b/homeassistant/components/rachio/translations/he.json
new file mode 100644
index 00000000000..5247b912cc4
--- /dev/null
+++ b/homeassistant/components/rachio/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "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": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py
index 4b89b52befe..171fd26b910 100644
--- a/homeassistant/components/rainmachine/binary_sensor.py
+++ b/homeassistant/components/rainmachine/binary_sensor.py
@@ -179,18 +179,8 @@ class ProvisionSettingsBinarySensor(RainMachineBinarySensor):
@callback
def update_from_latest_data(self) -> None:
"""Update the state."""
- if self._sensor_type == TYPE_FREEZE:
- self._state = self.coordinator.data["freeze"]
- elif self._sensor_type == TYPE_HOURLY:
- self._state = self.coordinator.data["hourly"]
- elif self._sensor_type == TYPE_MONTH:
- self._state = self.coordinator.data["month"]
- elif self._sensor_type == TYPE_RAINDELAY:
- self._state = self.coordinator.data["rainDelay"]
- elif self._sensor_type == TYPE_RAINSENSOR:
- self._state = self.coordinator.data["rainSensor"]
- elif self._sensor_type == TYPE_WEEKDAY:
- self._state = self.coordinator.data["weekDay"]
+ if self._sensor_type == TYPE_FLOW_SENSOR:
+ self._state = self.coordinator.data["system"].get("useFlowSensor")
class UniversalRestrictionsBinarySensor(RainMachineBinarySensor):
@@ -199,5 +189,7 @@ class UniversalRestrictionsBinarySensor(RainMachineBinarySensor):
@callback
def update_from_latest_data(self) -> None:
"""Update the state."""
- if self._sensor_type == TYPE_FLOW_SENSOR:
- self._state = self.coordinator.data["system"].get("useFlowSensor")
+ if self._sensor_type == TYPE_FREEZE_PROTECTION:
+ self._state = self.coordinator.data["freezeProtectEnabled"]
+ elif self._sensor_type == TYPE_HOT_DAYS:
+ self._state = self.coordinator.data["hotDaysExtraWatering"]
diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py
index a90091c6c3a..b0544b1adbe 100644
--- a/homeassistant/components/rainmachine/switch.py
+++ b/homeassistant/components/rainmachine/switch.py
@@ -84,9 +84,10 @@ SLOPE_TYPE_MAP = {
SPRINKLER_TYPE_MAP = {
0: "Not Set",
1: "Popup Spray",
- 2: "Rotors",
+ 2: "Rotors Low Rate",
3: "Surface Drip",
4: "Bubblers Drip",
+ 5: "Rotors High Rate",
99: "Other",
}
@@ -94,14 +95,16 @@ SUN_EXPOSURE_MAP = {0: "Not Set", 1: "Full Sun", 2: "Partial Shade", 3: "Full Sh
VEGETATION_MAP = {
0: "Not Set",
+ 1: "Not Set",
2: "Cool Season Grass",
3: "Fruit Trees",
4: "Flowers",
5: "Vegetables",
6: "Citrus",
- 7: "Trees and Bushes",
+ 7: "Bushes",
9: "Drought Tolerant Plants",
10: "Warm Season Grass",
+ 11: "Trees",
99: "Other",
}
@@ -386,7 +389,7 @@ class RainMachineZone(RainMachineSwitch):
ATTR_PRECIP_RATE: self._data.get("waterSense").get("precipitationRate"),
ATTR_RESTRICTIONS: self._data.get("restriction"),
ATTR_SLOPE: SLOPE_TYPE_MAP.get(self._data.get("slope")),
- ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(self._data.get("sun")),
+ ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(self._data.get("soil")),
ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(self._data.get("group_id")),
ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(self._data.get("sun")),
ATTR_TIME_REMAINING: self._data.get("remaining"),
diff --git a/homeassistant/components/rainmachine/translations/he.json b/homeassistant/components/rainmachine/translations/he.json
index 3007c0e968c..0f8dcfb7b28 100644
--- a/homeassistant/components/rainmachine/translations/he.json
+++ b/homeassistant/components/rainmachine/translations/he.json
@@ -1,9 +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": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "flow_title": "{ip}",
"step": {
"user": {
"data": {
- "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ "ip_address": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
}
}
}
diff --git a/homeassistant/components/rainmachine/translations/hu.json b/homeassistant/components/rainmachine/translations/hu.json
index 48718980e2e..1ff7dc34b9c 100644
--- a/homeassistant/components/rainmachine/translations/hu.json
+++ b/homeassistant/components/rainmachine/translations/hu.json
@@ -6,6 +6,7 @@
"error": {
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
+ "flow_title": "{ip}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/recollect_waste/translations/he.json b/homeassistant/components/recollect_waste/translations/he.json
new file mode 100644
index 00000000000..cdb921611c4
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/he.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index 0d6dddfa2d5..c16d7a2d198 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -306,7 +306,7 @@ def _async_register_services(hass, instance):
class PurgeTask(NamedTuple):
"""Object to store information about purge task."""
- keep_days: int
+ purge_before: datetime
repack: bool
apply_filter: bool
@@ -451,7 +451,8 @@ class Recorder(threading.Thread):
repack = kwargs.get(ATTR_REPACK)
apply_filter = kwargs.get(ATTR_APPLY_FILTER)
- self.queue.put(PurgeTask(keep_days, repack, 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):
"""Trigger an adhoc purge of requested entities."""
@@ -538,7 +539,8 @@ class Recorder(threading.Thread):
# Purge will schedule the perodic cleanups
# after it completes to ensure it does not happen
# until after the database is vacuumed
- self.queue.put(PurgeTask(self.keep_days, repack=False, apply_filter=False))
+ purge_before = dt_util.utcnow() - timedelta(days=self.keep_days)
+ self.queue.put(PurgeTask(purge_before, repack=False, apply_filter=False))
else:
self.queue.put(PerodicCleanupTask())
@@ -696,16 +698,16 @@ class Recorder(threading.Thread):
self.migration_in_progress = False
persistent_notification.dismiss(self.hass, "recorder_database_migration")
- def _run_purge(self, keep_days, repack, apply_filter):
+ def _run_purge(self, purge_before, repack, apply_filter):
"""Purge the database."""
- if purge.purge_old_data(self, keep_days, repack, apply_filter):
+ if purge.purge_old_data(self, purge_before, repack, apply_filter):
# We always need to do the db cleanups after a purge
# is finished to ensure the WAL checkpoint and other
# tasks happen after a vacuum.
perodic_db_cleanups(self)
return
# Schedule a new purge task if this one didn't finish
- self.queue.put(PurgeTask(keep_days, repack, apply_filter))
+ self.queue.put(PurgeTask(purge_before, repack, apply_filter))
def _run_purge_entities(self, entity_filter):
"""Purge entities from the database."""
@@ -724,7 +726,7 @@ class Recorder(threading.Thread):
def _process_one_event(self, event):
"""Process one event."""
if isinstance(event, PurgeTask):
- self._run_purge(event.keep_days, event.repack, event.apply_filter)
+ self._run_purge(event.purge_before, event.repack, event.apply_filter)
return
if isinstance(event, PurgeEntitiesTask):
self._run_purge_entities(event.entity_filter)
diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json
index 6a3f6ae6b54..7e4c9d9b9fa 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.13"],
+ "requirements": ["sqlalchemy==1.4.17"],
"codeowners": [],
"quality_scale": "internal",
"iot_class": "local_push"
diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py
index 02c74635f03..2ed676bfdb9 100644
--- a/homeassistant/components/recorder/migration.py
+++ b/homeassistant/components/recorder/migration.py
@@ -11,7 +11,14 @@ from sqlalchemy.exc import (
)
from sqlalchemy.schema import AddConstraint, DropConstraint
-from .models import SCHEMA_VERSION, TABLE_STATES, Base, SchemaChanges, Statistics
+from .models import (
+ SCHEMA_VERSION,
+ TABLE_STATES,
+ Base,
+ SchemaChanges,
+ Statistics,
+ StatisticsMeta,
+)
from .util import session_scope
_LOGGER = logging.getLogger(__name__)
@@ -444,14 +451,30 @@ def _apply_update(engine, session, new_version, old_version):
elif new_version == 14:
_modify_columns(connection, engine, "events", ["event_type VARCHAR(64)"])
elif new_version == 15:
- if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__):
- # Recreate the statistics table
- Statistics.__table__.drop(engine)
- Statistics.__table__.create(engine)
+ # This dropped the statistics table, done again in version 18.
+ pass
elif new_version == 16:
_drop_foreign_key_constraints(
connection, engine, TABLE_STATES, ["old_state_id"]
)
+ elif new_version == 17:
+ # This dropped the statistics table, done again in version 18.
+ pass
+ elif new_version == 18:
+ # Recreate the statistics and statistics meta tables.
+ #
+ # Order matters! Statistics has a relation with StatisticsMeta,
+ # so statistics need to be deleted before meta (or in pair depending
+ # on the SQL backend); and meta needs to be created before statistics.
+ if sqlalchemy.inspect(engine).has_table(
+ StatisticsMeta.__tablename__
+ ) or sqlalchemy.inspect(engine).has_table(Statistics.__tablename__):
+ Base.metadata.drop_all(
+ bind=engine, tables=[Statistics.__table__, StatisticsMeta.__table__]
+ )
+
+ StatisticsMeta.__table__.create(engine)
+ Statistics.__table__.create(engine)
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 ac3f6c9e401..c77d824c64f 100644
--- a/homeassistant/components/recorder/models.py
+++ b/homeassistant/components/recorder/models.py
@@ -36,7 +36,7 @@ import homeassistant.util.dt as dt_util
# pylint: disable=invalid-name
Base = declarative_base()
-SCHEMA_VERSION = 16
+SCHEMA_VERSION = 18
_LOGGER = logging.getLogger(__name__)
@@ -47,6 +47,7 @@ TABLE_STATES = "states"
TABLE_RECORDER_RUNS = "recorder_runs"
TABLE_SCHEMA_CHANGES = "schema_changes"
TABLE_STATISTICS = "statistics"
+TABLE_STATISTICS_META = "statistics_meta"
ALL_TABLES = [
TABLE_STATES,
@@ -54,6 +55,7 @@ ALL_TABLES = [
TABLE_RECORDER_RUNS,
TABLE_SCHEMA_CHANGES,
TABLE_STATISTICS,
+ TABLE_STATISTICS_META,
]
DATETIME_TYPE = DateTime(timezone=True).with_variant(
@@ -64,10 +66,12 @@ DATETIME_TYPE = DateTime(timezone=True).with_variant(
class Events(Base): # type: ignore
"""Event history data."""
- __table_args__ = {
- "mysql_default_charset": "utf8mb4",
- "mysql_collate": "utf8mb4_unicode_ci",
- }
+ __table_args__ = (
+ # Used for fetching events at a specific time
+ # see logbook
+ Index("ix_events_event_type_time_fired", "event_type", "time_fired"),
+ {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"},
+ )
__tablename__ = TABLE_EVENTS
event_id = Column(Integer, Identity(), primary_key=True)
event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE))
@@ -79,12 +83,6 @@ class Events(Base): # type: ignore
context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True)
context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True)
- __table_args__ = (
- # Used for fetching events at a specific time
- # see logbook
- Index("ix_events_event_type_time_fired", "event_type", "time_fired"),
- )
-
def __repr__(self) -> str:
"""Return string representation of instance for debugging."""
return (
@@ -131,10 +129,12 @@ class Events(Base): # type: ignore
class States(Base): # type: ignore
"""State change history."""
- __table_args__ = {
- "mysql_default_charset": "utf8mb4",
- "mysql_collate": "utf8mb4_unicode_ci",
- }
+ __table_args__ = (
+ # Used for fetching the state of entities at a specific time
+ # (get_states in history.py)
+ Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"),
+ {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"},
+ )
__tablename__ = TABLE_STATES
state_id = Column(Integer, Identity(), primary_key=True)
domain = Column(String(MAX_LENGTH_STATE_DOMAIN))
@@ -151,12 +151,6 @@ class States(Base): # type: ignore
event = relationship("Events", uselist=False)
old_state = relationship("States", remote_side=[state_id])
- __table_args__ = (
- # Used for fetching the state of entities at a specific time
- # (get_states in history.py)
- Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"),
- )
-
def __repr__(self) -> str:
"""Return string representation of instance for debugging."""
return (
@@ -215,15 +209,18 @@ class States(Base): # type: ignore
class Statistics(Base): # type: ignore
"""Statistics."""
- __table_args__ = {
- "mysql_default_charset": "utf8mb4",
- "mysql_collate": "utf8mb4_unicode_ci",
- }
+ __table_args__ = (
+ # Used for fetching statistics for a certain entity at a specific time
+ Index("ix_statistics_statistic_id_start", "metadata_id", "start"),
+ )
__tablename__ = TABLE_STATISTICS
id = Column(Integer, primary_key=True)
created = Column(DATETIME_TYPE, default=dt_util.utcnow)
- source = Column(String(32))
- statistic_id = Column(String(255))
+ metadata_id = Column(
+ Integer,
+ ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"),
+ index=True,
+ )
start = Column(DATETIME_TYPE, index=True)
mean = Column(Float())
min = Column(Float())
@@ -232,25 +229,43 @@ class Statistics(Base): # type: ignore
state = Column(Float())
sum = Column(Float())
- __table_args__ = (
- # Used for fetching statistics for a certain entity at a specific time
- Index("ix_statistics_statistic_id_start", "statistic_id", "start"),
- )
-
@staticmethod
- def from_stats(source, statistic_id, start, stats):
+ def from_stats(metadata_id, start, stats):
"""Create object from a statistics."""
return Statistics(
- source=source,
- statistic_id=statistic_id,
+ metadata_id=metadata_id,
start=start,
**stats,
)
+class StatisticsMeta(Base): # type: ignore
+ """Statistics meta data."""
+
+ __tablename__ = TABLE_STATISTICS_META
+ id = Column(Integer, primary_key=True)
+ statistic_id = Column(String(255), index=True)
+ source = Column(String(32))
+ unit_of_measurement = Column(String(255))
+ has_mean = Column(Boolean)
+ has_sum = Column(Boolean)
+
+ @staticmethod
+ def from_meta(source, statistic_id, unit_of_measurement, has_mean, has_sum):
+ """Create object from meta data."""
+ return StatisticsMeta(
+ source=source,
+ statistic_id=statistic_id,
+ unit_of_measurement=unit_of_measurement,
+ has_mean=has_mean,
+ has_sum=has_sum,
+ )
+
+
class RecorderRuns(Base): # type: ignore
"""Representation of recorder run."""
+ __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),)
__tablename__ = TABLE_RECORDER_RUNS
run_id = Column(Integer, Identity(), primary_key=True)
start = Column(DateTime(timezone=True), default=dt_util.utcnow)
@@ -258,8 +273,6 @@ class RecorderRuns(Base): # type: ignore
closed_incorrect = Column(Boolean, default=False)
created = Column(DateTime(timezone=True), default=dt_util.utcnow)
- __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),)
-
def __repr__(self) -> str:
"""Return string representation of instance for debugging."""
end = (
diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py
index e1cf15e331d..49803117119 100644
--- a/homeassistant/components/recorder/purge.py
+++ b/homeassistant/components/recorder/purge.py
@@ -1,15 +1,13 @@
"""Purge old data helper."""
from __future__ import annotations
-from datetime import datetime, timedelta
+from datetime import datetime
import logging
from typing import TYPE_CHECKING, Callable
from sqlalchemy.orm.session import Session
from sqlalchemy.sql.expression import distinct
-import homeassistant.util.dt as dt_util
-
from .const import MAX_ROWS_TO_PURGE
from .models import Events, RecorderRuns, States
from .repack import repack_database
@@ -23,13 +21,12 @@ _LOGGER = logging.getLogger(__name__)
@retryable_database_job("purge")
def purge_old_data(
- instance: Recorder, purge_days: int, repack: bool, apply_filter: bool = False
+ instance: Recorder, purge_before: datetime, repack: bool, apply_filter: bool = False
) -> bool:
- """Purge events and states older than purge_days ago.
+ """Purge events and states older than purge_before.
Cleans up an timeframe of an hour, based on the oldest record.
"""
- purge_before = dt_util.utcnow() - timedelta(days=purge_days)
_LOGGER.debug(
"Purging states and events before target %s",
purge_before.isoformat(sep=" ", timespec="seconds"),
diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py
index ee733039d43..2ef49df7ded 100644
--- a/homeassistant/components/recorder/statistics.py
+++ b/homeassistant/components/recorder/statistics.py
@@ -10,17 +10,20 @@ from typing import TYPE_CHECKING
from sqlalchemy import bindparam
from sqlalchemy.ext import baked
+from homeassistant.const import PRESSURE_PA, TEMP_CELSIUS
import homeassistant.util.dt as dt_util
+import homeassistant.util.pressure as pressure_util
+import homeassistant.util.temperature as temperature_util
from .const import DOMAIN
-from .models import Statistics, process_timestamp_to_utc_isoformat
+from .models import Statistics, StatisticsMeta, process_timestamp_to_utc_isoformat
from .util import execute, retryable_database_job, session_scope
if TYPE_CHECKING:
from . import Recorder
QUERY_STATISTICS = [
- Statistics.statistic_id,
+ Statistics.metadata_id,
Statistics.start,
Statistics.mean,
Statistics.min,
@@ -30,7 +33,29 @@ QUERY_STATISTICS = [
Statistics.sum,
]
+QUERY_STATISTIC_META = [
+ StatisticsMeta.id,
+ StatisticsMeta.statistic_id,
+ StatisticsMeta.unit_of_measurement,
+]
+
STATISTICS_BAKERY = "recorder_statistics_bakery"
+STATISTICS_META_BAKERY = "recorder_statistics_bakery"
+
+# Convert pressure and temperature statistics from the native unit used for statistics
+# to the units configured by the user
+UNIT_CONVERSIONS = {
+ PRESSURE_PA: lambda x, units: pressure_util.convert(
+ x, PRESSURE_PA, units.pressure_unit
+ )
+ if x is not None
+ else None,
+ TEMP_CELSIUS: lambda x, units: temperature_util.convert(
+ x, TEMP_CELSIUS, units.temperature_unit
+ )
+ if x is not None
+ else None,
+}
_LOGGER = logging.getLogger(__name__)
@@ -38,6 +63,7 @@ _LOGGER = logging.getLogger(__name__)
def async_setup(hass):
"""Set up the history hooks."""
hass.data[STATISTICS_BAKERY] = baked.bakery()
+ hass.data[STATISTICS_META_BAKERY] = baked.bakery()
def get_start_time() -> datetime.datetime:
@@ -47,16 +73,39 @@ def get_start_time() -> datetime.datetime:
return start
+def _get_metadata_ids(hass, session, statistic_ids):
+ """Resolve metadata_id for a list of statistic_ids."""
+ baked_query = hass.data[STATISTICS_META_BAKERY](
+ lambda session: session.query(*QUERY_STATISTIC_META)
+ )
+ baked_query += lambda q: q.filter(
+ StatisticsMeta.statistic_id.in_(bindparam("statistic_ids"))
+ )
+ result = execute(baked_query(session).params(statistic_ids=statistic_ids))
+
+ return [id for id, _, _ in result]
+
+
+def _get_or_add_metadata_id(hass, session, statistic_id, metadata):
+ """Get metadata_id for a statistic_id, add if it doesn't exist."""
+ metadata_id = _get_metadata_ids(hass, session, [statistic_id])
+ if not metadata_id:
+ unit = metadata["unit_of_measurement"]
+ has_mean = metadata["has_mean"]
+ has_sum = metadata["has_sum"]
+ session.add(
+ StatisticsMeta.from_meta(DOMAIN, statistic_id, unit, has_mean, has_sum)
+ )
+ metadata_id = _get_metadata_ids(hass, session, [statistic_id])
+ return metadata_id[0]
+
+
@retryable_database_job("statistics")
def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool:
"""Compile statistics."""
start = dt_util.as_utc(start)
end = start + timedelta(hours=1)
- _LOGGER.debug(
- "Compiling statistics for %s-%s",
- start,
- end,
- )
+ _LOGGER.debug("Compiling statistics for %s-%s", start, end)
platform_stats = []
for domain, platform in instance.hass.data[DOMAIN].items():
if not hasattr(platform, "compile_statistics"):
@@ -69,14 +118,71 @@ def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool:
with session_scope(session=instance.get_session()) as session: # type: ignore
for stats in platform_stats:
for entity_id, stat in stats.items():
- session.add(Statistics.from_stats(DOMAIN, entity_id, start, stat))
+ metadata_id = _get_or_add_metadata_id(
+ instance.hass, session, entity_id, stat["meta"]
+ )
+ session.add(Statistics.from_stats(metadata_id, start, stat["stat"]))
return True
-def statistics_during_period(hass, start_time, end_time=None, statistic_id=None):
- """Return states changes during UTC period start_time - end_time."""
+def _get_metadata(hass, session, statistic_ids, statistic_type):
+ """Fetch meta data."""
+
+ def _meta(metas, wanted_metadata_id):
+ meta = None
+ for metadata_id, statistic_id, unit in metas:
+ if metadata_id == wanted_metadata_id:
+ meta = {"unit_of_measurement": unit, "statistic_id": statistic_id}
+ return meta
+
+ baked_query = hass.data[STATISTICS_META_BAKERY](
+ lambda session: session.query(*QUERY_STATISTIC_META)
+ )
+ if statistic_ids is not None:
+ baked_query += lambda q: q.filter(
+ StatisticsMeta.statistic_id.in_(bindparam("statistic_ids"))
+ )
+ if statistic_type == "mean":
+ baked_query += lambda q: q.filter(StatisticsMeta.has_mean.isnot(False))
+ if statistic_type == "sum":
+ baked_query += lambda q: q.filter(StatisticsMeta.has_sum.isnot(False))
+ result = execute(baked_query(session).params(statistic_ids=statistic_ids))
+
+ metadata_ids = [metadata[0] for metadata in result]
+ return {id: _meta(result, id) for id in metadata_ids}
+
+
+def _configured_unit(unit: str, units) -> str:
+ """Return the pressure and temperature units configured by the user."""
+ if unit == PRESSURE_PA:
+ return units.pressure_unit
+ if unit == TEMP_CELSIUS:
+ return units.temperature_unit
+ return unit
+
+
+def list_statistic_ids(hass, statistic_type=None):
+ """Return statistic_ids and meta data."""
+ units = hass.config.units
with session_scope(hass=hass) as session:
+ metadata = _get_metadata(hass, session, None, statistic_type)
+
+ for meta in metadata.values():
+ unit = _configured_unit(meta["unit_of_measurement"], units)
+ meta["unit_of_measurement"] = unit
+
+ return list(metadata.values())
+
+
+def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None):
+ """Return states changes during UTC period start_time - end_time."""
+ metadata = None
+ with session_scope(hass=hass) as session:
+ metadata = _get_metadata(hass, session, statistic_ids, None)
+ if not metadata:
+ return {}
+
baked_query = hass.data[STATISTICS_BAKERY](
lambda session: session.query(*QUERY_STATISTICS)
)
@@ -86,81 +192,90 @@ def statistics_during_period(hass, start_time, end_time=None, statistic_id=None)
if end_time is not None:
baked_query += lambda q: q.filter(Statistics.start < bindparam("end_time"))
- if statistic_id is not None:
- baked_query += lambda q: q.filter_by(statistic_id=bindparam("statistic_id"))
- statistic_id = statistic_id.lower()
+ metadata_ids = None
+ if statistic_ids is not None:
+ baked_query += lambda q: q.filter(
+ Statistics.metadata_id.in_(bindparam("metadata_ids"))
+ )
+ metadata_ids = list(metadata.keys())
- baked_query += lambda q: q.order_by(Statistics.statistic_id, Statistics.start)
+ baked_query += lambda q: q.order_by(Statistics.metadata_id, Statistics.start)
stats = execute(
baked_query(session).params(
- start_time=start_time, end_time=end_time, statistic_id=statistic_id
+ start_time=start_time, end_time=end_time, metadata_ids=metadata_ids
)
)
-
- statistic_ids = [statistic_id] if statistic_id is not None else None
-
- return _sorted_statistics_to_dict(stats, statistic_ids)
+ return _sorted_statistics_to_dict(hass, stats, statistic_ids, metadata)
-def get_last_statistics(hass, number_of_stats, statistic_id=None):
- """Return the last number_of_stats statistics."""
+def get_last_statistics(hass, number_of_stats, statistic_id):
+ """Return the last number_of_stats statistics for a statistic_id."""
+ statistic_ids = [statistic_id]
with session_scope(hass=hass) as session:
+ metadata = _get_metadata(hass, session, statistic_ids, None)
+ if not metadata:
+ return {}
+
baked_query = hass.data[STATISTICS_BAKERY](
lambda session: session.query(*QUERY_STATISTICS)
)
- if statistic_id is not None:
- baked_query += lambda q: q.filter_by(statistic_id=bindparam("statistic_id"))
+ baked_query += lambda q: q.filter_by(metadata_id=bindparam("metadata_id"))
+ metadata_id = next(iter(metadata.keys()))
baked_query += lambda q: q.order_by(
- Statistics.statistic_id, Statistics.start.desc()
+ Statistics.metadata_id, Statistics.start.desc()
)
baked_query += lambda q: q.limit(bindparam("number_of_stats"))
stats = execute(
baked_query(session).params(
- number_of_stats=number_of_stats, statistic_id=statistic_id
+ number_of_stats=number_of_stats, metadata_id=metadata_id
)
)
- statistic_ids = [statistic_id] if statistic_id is not None else None
-
- return _sorted_statistics_to_dict(stats, statistic_ids)
+ return _sorted_statistics_to_dict(hass, stats, statistic_ids, metadata)
def _sorted_statistics_to_dict(
+ hass,
stats,
statistic_ids,
+ metadata,
):
"""Convert SQL results into JSON friendly data structure."""
result = defaultdict(list)
+ units = hass.config.units
+
# Set all statistic IDs to empty lists in result set to maintain the order
if statistic_ids is not None:
for stat_id in statistic_ids:
result[stat_id] = []
- # Called in a tight loop so cache the function
- # here
+ # Called in a tight loop so cache the function here
_process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat
- # Append all changes to it
- for ent_id, group in groupby(stats, lambda state: state.statistic_id):
- ent_results = result[ent_id]
+ # Append all statistic entries, and do unit conversion
+ for meta_id, group in groupby(stats, lambda state: state.metadata_id):
+ unit = metadata[meta_id]["unit_of_measurement"]
+ statistic_id = metadata[meta_id]["statistic_id"]
+ convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x)
+ ent_results = result[meta_id]
ent_results.extend(
{
- "statistic_id": db_state.statistic_id,
+ "statistic_id": statistic_id,
"start": _process_timestamp_to_utc_isoformat(db_state.start),
- "mean": db_state.mean,
- "min": db_state.min,
- "max": db_state.max,
+ "mean": convert(db_state.mean, units),
+ "min": convert(db_state.min, units),
+ "max": convert(db_state.max, units),
"last_reset": _process_timestamp_to_utc_isoformat(db_state.last_reset),
- "state": db_state.state,
- "sum": db_state.sum,
+ "state": convert(db_state.state, units),
+ "sum": convert(db_state.sum, units),
}
for db_state in group
)
# Filter out the empty lists if some states had 0 results.
- return {key: val for key, val in result.items() if val}
+ return {metadata[key]["statistic_id"]: val for key, val in result.items() if val}
diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py
index db9fb46425b..80c90ccaa20 100644
--- a/homeassistant/components/recorder/util.py
+++ b/homeassistant/components/recorder/util.py
@@ -22,6 +22,7 @@ from .models import (
TABLE_RECORDER_RUNS,
TABLE_SCHEMA_CHANGES,
TABLE_STATISTICS,
+ TABLE_STATISTICS_META,
RecorderRuns,
process_timestamp,
)
@@ -179,7 +180,7 @@ def basic_sanity_check(cursor):
"""Check tables to make sure select does not fail."""
for table in ALL_TABLES:
- if table == TABLE_STATISTICS:
+ if table in [TABLE_STATISTICS, TABLE_STATISTICS_META]:
continue
if table in (TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES):
cursor.execute(f"SELECT * FROM {table};") # nosec # not injection
diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py
index fef0da4dae6..0fc4255615e 100644
--- a/homeassistant/components/remote/__init__.py
+++ b/homeassistant/components/remote/__init__.py
@@ -145,20 +145,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
class RemoteEntity(ToggleEntity):
"""Base class for remote entities."""
+ _attr_activity_list: list[str] | None = None
+ _attr_current_activity: str | None = None
+ _attr_supported_features: int = 0
+
@property
def supported_features(self) -> int:
"""Flag supported features."""
- return 0
+ return self._attr_supported_features
@property
def current_activity(self) -> str | None:
"""Active activity."""
- return None
+ return self._attr_current_activity
@property
def activity_list(self) -> list[str] | None:
"""List of available activities."""
- return None
+ return self._attr_activity_list
@final
@property
diff --git a/homeassistant/components/remote/translations/he.json b/homeassistant/components/remote/translations/he.json
index 816e0fd96bf..4b6283c1811 100644
--- a/homeassistant/components/remote/translations/he.json
+++ b/homeassistant/components/remote/translations/he.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "\u05d4\u05d7\u05dc\u05e3 \u05de\u05e6\u05d1 {entity_name}",
+ "turn_off": "\u05db\u05d1\u05d4 \u05d0\u05ea {entity_name}",
+ "turn_on": "\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9",
+ "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4",
+ "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc"
+ }
+ },
"state": {
"_": {
"off": "\u05db\u05d1\u05d5\u05d9",
diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py
index c78b0c6f944..9cff8377c35 100644
--- a/homeassistant/components/rflink/__init__.py
+++ b/homeassistant/components/rflink/__init__.py
@@ -275,8 +275,6 @@ async def async_setup(hass, config):
except (
SerialException,
- ConnectionRefusedError,
- TimeoutError,
OSError,
asyncio.TimeoutError,
) as exc:
diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json
index 0f3837f3a59..a806afb6dbf 100644
--- a/homeassistant/components/rfxtrx/translations/de.json
+++ b/homeassistant/components/rfxtrx/translations/de.json
@@ -50,6 +50,7 @@
"automatic_add": "Automatisches Hinzuf\u00fcgen aktivieren",
"debug": "Debugging aktivieren",
"device": "Zu konfigurierendes Ger\u00e4t ausw\u00e4hlen",
+ "event_code": "Ereigniscode zum Hinzuf\u00fcgen eingeben",
"remove_device": "Zu l\u00f6schendes Ger\u00e4t ausw\u00e4hlen"
},
"title": "Rfxtrx Optionen"
@@ -65,7 +66,8 @@
"replace_device": "W\u00e4hle ein Ger\u00e4t aus, das ersetzt werden soll",
"signal_repetitions": "Anzahl der Signalwiederholungen",
"venetian_blind_mode": "Jalousie-Modus"
- }
+ },
+ "title": "Ger\u00e4teoptionen konfigurieren"
}
}
}
diff --git a/homeassistant/components/rfxtrx/translations/he.json b/homeassistant/components/rfxtrx/translations/he.json
new file mode 100644
index 00000000000..cabe3734e11
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/he.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\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.",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ },
+ "setup_serial": {
+ "data": {
+ "device": "\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df"
+ },
+ "title": "\u05d4\u05ea\u05e7\u05df"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB"
+ },
+ "title": "\u05e0\u05ea\u05d9\u05d1"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json
index 938c471e992..4d2ae4710e7 100644
--- a/homeassistant/components/rfxtrx/translations/it.json
+++ b/homeassistant/components/rfxtrx/translations/it.json
@@ -5,9 +5,13 @@
"cannot_connect": "Impossibile connettersi"
},
"error": {
- "cannot_connect": "Impossibile connettersi"
+ "cannot_connect": "Impossibile connettersi",
+ "one": "Pi\u00f9",
+ "other": "Altri"
},
"step": {
+ "one": "Pi\u00f9",
+ "other": "Altri",
"setup_network": {
"data": {
"host": "Host",
@@ -35,6 +39,7 @@
}
}
},
+ "one": "Pi\u00f9",
"options": {
"error": {
"already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato",
@@ -70,5 +75,6 @@
"title": "Configurare le opzioni del dispositivo"
}
}
- }
+ },
+ "other": "Altri"
}
\ No newline at end of file
diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py
index 8f827aee7d2..580fc71e141 100644
--- a/homeassistant/components/ring/camera.py
+++ b/homeassistant/components/ring/camera.py
@@ -18,7 +18,7 @@ from homeassistant.util import dt as dt_util
from . import ATTRIBUTION, DOMAIN
from .entity import RingEntityMixin
-FORCE_REFRESH_INTERVAL = timedelta(minutes=45)
+FORCE_REFRESH_INTERVAL = timedelta(minutes=3)
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py
index d4cc6796bf1..cca0c231d96 100644
--- a/homeassistant/components/ring/config_flow.py
+++ b/homeassistant/components/ring/config_flow.py
@@ -64,7 +64,9 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user",
- data_schema=vol.Schema({"username": str, "password": str}),
+ data_schema=vol.Schema(
+ {vol.Required("username"): str, vol.Required("password"): str}
+ ),
errors=errors,
)
@@ -75,7 +77,7 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="2fa",
- data_schema=vol.Schema({"2fa": str}),
+ data_schema=vol.Schema({vol.Required("2fa"): str}),
)
diff --git a/homeassistant/components/ring/translations/he.json b/homeassistant/components/ring/translations/he.json
index ac90b3264ea..fe6357d0150 100644
--- a/homeassistant/components/ring/translations/he.json
+++ b/homeassistant/components/ring/translations/he.json
@@ -1,5 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py
index 48c50f9cc46..4b33873e88d 100644
--- a/homeassistant/components/risco/__init__.py
+++ b/homeassistant/components/risco/__init__.py
@@ -27,7 +27,7 @@ LAST_EVENT_TIMESTAMP_KEY = "last_event_timestamp"
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Risco from a config entry."""
data = entry.data
risco = RiscoAPI(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN])
diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py
index 0bc9c49707a..c20aa2af287 100644
--- a/homeassistant/components/risco/config_flow.py
+++ b/homeassistant/components/risco/config_flow.py
@@ -30,7 +30,13 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
-DATA_SCHEMA = vol.Schema({CONF_USERNAME: str, CONF_PASSWORD: str, CONF_PIN: str})
+DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ vol.Required(CONF_PIN): str,
+ }
+)
HA_STATES = [
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json
index 8e50b61f16f..424a93f3eb7 100644
--- a/homeassistant/components/risco/translations/de.json
+++ b/homeassistant/components/risco/translations/de.json
@@ -33,8 +33,10 @@
"init": {
"data": {
"code_arm_required": "PIN-Code zum Entsperren vorgeben",
- "code_disarm_required": "PIN-Code zum Entsperren vorgeben"
- }
+ "code_disarm_required": "PIN-Code zum Entsperren vorgeben",
+ "scan_interval": "Wie oft Risco abgefragt werden soll (in Sekunden)"
+ },
+ "title": "Optionen konfigurieren"
},
"risco_to_ha": {
"data": {
diff --git a/homeassistant/components/risco/translations/he.json b/homeassistant/components/risco/translations/he.json
new file mode 100644
index 00000000000..08c5ec7d4c0
--- /dev/null
+++ b/homeassistant/components/risco/translations/he.json
@@ -0,0 +1,40 @@
+{
+ "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",
+ "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": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "pin": "\u05e7\u05d5\u05d3 PIN",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "ha_to_risco": {
+ "data": {
+ "armed_away": "\u05d3\u05e8\u05d5\u05da \u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea",
+ "armed_custom_bypass": "\u05d3\u05e8\u05d5\u05da \u05de\u05e2\u05e7\u05e3 \u05de\u05d5\u05ea\u05d0\u05dd \u05d0\u05d9\u05e9\u05d9\u05ea",
+ "armed_home": "\u05d4\u05d1\u05d9\u05ea \u05d3\u05e8\u05d5\u05da",
+ "armed_night": "\u05d3\u05e8\u05d5\u05da \u05dc\u05d9\u05dc\u05d4"
+ }
+ },
+ "risco_to_ha": {
+ "data": {
+ "A": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d0'",
+ "B": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d1'",
+ "C": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d2'"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py
index 65c1a2dd97c..84fc5ed2cf5 100644
--- a/homeassistant/components/rituals_perfume_genie/__init__.py
+++ b/homeassistant/components/rituals_perfume_genie/__init__.py
@@ -13,19 +13,17 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN, HUBLOT
-PLATFORMS = ["binary_sensor", "sensor", "switch"]
-
-EMPTY_CREDENTIALS = ""
+PLATFORMS = ["binary_sensor", "number", "select", "sensor", "switch"]
_LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(seconds=30)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Rituals Perfume Genie from a config entry."""
session = async_get_clientsession(hass)
- account = Account(EMPTY_CREDENTIALS, EMPTY_CREDENTIALS, session)
+ account = Account(session=session)
account.data = {ACCOUNT_HASH: entry.data.get(ACCOUNT_HASH)}
try:
@@ -62,10 +60,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
class RitualsDataUpdateCoordinator(DataUpdateCoordinator):
- """Class to manage fetching Rituals Perufme Genie device data from single endpoint."""
+ """Class to manage fetching Rituals Perfume Genie device data from single endpoint."""
def __init__(self, hass: HomeAssistant, device: Diffuser) -> None:
- """Initialize global Rituals Perufme Genie data updater."""
+ """Initialize global Rituals Perfume Genie data updater."""
self._device = device
super().__init__(
hass,
diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py
index 2d82982388d..a529ff3dca6 100644
--- a/homeassistant/components/rituals_perfume_genie/binary_sensor.py
+++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py
@@ -16,7 +16,6 @@ from .const import COORDINATORS, DEVICES, DOMAIN
from .entity import DiffuserEntity
CHARGING_SUFFIX = " Battery Charging"
-BATTERY_CHARGING_ID = 21
async def async_setup_entry(
@@ -27,18 +26,19 @@ async def async_setup_entry(
"""Set up the diffuser binary sensors."""
diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES]
coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS]
- entities = []
- for hublot, diffuser in diffusers.items():
- if diffuser.has_battery:
- coordinator = coordinators[hublot]
- entities.append(DiffuserBatteryChargingBinarySensor(diffuser, coordinator))
- async_add_entities(entities)
+ async_add_entities(
+ DiffuserBatteryChargingBinarySensor(diffuser, coordinators[hublot])
+ for hublot, diffuser in diffusers.items()
+ if diffuser.has_battery
+ )
class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity):
"""Representation of a diffuser battery charging binary sensor."""
+ _attr_device_class = DEVICE_CLASS_BATTERY_CHARGING
+
def __init__(
self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator
) -> None:
@@ -49,8 +49,3 @@ class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity):
def is_on(self) -> bool:
"""Return the state of the battery charging binary sensor."""
return self._diffuser.charging
-
- @property
- def device_class(self) -> str:
- """Return the device class of the battery charging binary sensor."""
- return DEVICE_CLASS_BATTERY_CHARGING
diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py
index c0bf72fb90e..bafdef9140c 100644
--- a/homeassistant/components/rituals_perfume_genie/const.py
+++ b/homeassistant/components/rituals_perfume_genie/const.py
@@ -5,7 +5,5 @@ COORDINATORS = "coordinators"
DEVICES = "devices"
ACCOUNT_HASH = "account_hash"
-ATTRIBUTES = "attributes"
HUBLOT = "hublot"
-ID = "id"
SENSORS = "sensors"
diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py
index 1c1f3912c68..19c3f3cd424 100644
--- a/homeassistant/components/rituals_perfume_genie/entity.py
+++ b/homeassistant/components/rituals_perfume_genie/entity.py
@@ -3,16 +3,16 @@ from __future__ import annotations
from pyrituals import Diffuser
-from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import RitualsDataUpdateCoordinator
-from .const import ATTRIBUTES, DOMAIN, HUBLOT, SENSORS
+from .const import DOMAIN, HUBLOT, SENSORS
MANUFACTURER = "Rituals Cosmetics"
MODEL = "The Perfume Genie"
MODEL2 = "The Perfume Genie 2.0"
+ATTRIBUTES = "attributes"
ROOMNAME = "roomnamec"
STATUS = "status"
VERSION = "versionc"
@@ -34,32 +34,21 @@ class DiffuserEntity(CoordinatorEntity):
"""Init from config, hookup diffuser and coordinator."""
super().__init__(coordinator)
self._diffuser = diffuser
- self._entity_suffix = entity_suffix
- self._hublot = self._diffuser.hub_data[HUBLOT]
- self._hubname = self._diffuser.hub_data[ATTRIBUTES][ROOMNAME]
- @property
- def unique_id(self) -> str:
- """Return the unique ID of the entity."""
- return f"{self._hublot}{self._entity_suffix}"
+ hublot = self._diffuser.hub_data[HUBLOT]
+ hubname = self._diffuser.hub_data[ATTRIBUTES][ROOMNAME]
- @property
- def name(self) -> str:
- """Return the name of the entity."""
- return f"{self._hubname}{self._entity_suffix}"
+ self._attr_name = f"{hubname}{entity_suffix}"
+ self._attr_unique_id = f"{hublot}{entity_suffix}"
+ self._attr_device_info = {
+ "name": hubname,
+ "identifiers": {(DOMAIN, hublot)},
+ "manufacturer": MANUFACTURER,
+ "model": MODEL if diffuser.has_battery else MODEL2,
+ "sw_version": diffuser.hub_data[SENSORS][VERSION],
+ }
@property
def available(self) -> bool:
"""Return if the entity is available."""
return super().available and self._diffuser.hub_data[STATUS] == AVAILABLE_STATE
-
- @property
- def device_info(self) -> DeviceInfo:
- """Return information about the device."""
- return {
- "name": self._hubname,
- "identifiers": {(DOMAIN, self._hublot)},
- "manufacturer": MANUFACTURER,
- "model": MODEL if self._diffuser.has_battery else MODEL2,
- "sw_version": self._diffuser.hub_data[SENSORS][VERSION],
- }
diff --git a/homeassistant/components/rituals_perfume_genie/manifest.json b/homeassistant/components/rituals_perfume_genie/manifest.json
index 756af10f33b..2736b960751 100644
--- a/homeassistant/components/rituals_perfume_genie/manifest.json
+++ b/homeassistant/components/rituals_perfume_genie/manifest.json
@@ -3,7 +3,7 @@
"name": "Rituals Perfume Genie",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie",
- "requirements": ["pyrituals==0.0.3"],
+ "requirements": ["pyrituals==0.0.4"],
"codeowners": ["@milanmeu"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/rituals_perfume_genie/number.py b/homeassistant/components/rituals_perfume_genie/number.py
new file mode 100644
index 00000000000..26ae393b071
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/number.py
@@ -0,0 +1,63 @@
+"""Support for Rituals Perfume Genie numbers."""
+from __future__ import annotations
+
+from pyrituals import Diffuser
+
+from homeassistant.components.number import NumberEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from . import RitualsDataUpdateCoordinator
+from .const import COORDINATORS, DEVICES, DOMAIN
+from .entity import DiffuserEntity
+
+MIN_PERFUME_AMOUNT = 1
+MAX_PERFUME_AMOUNT = 3
+
+PERFUME_AMOUNT_SUFFIX = " Perfume Amount"
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up the diffuser numbers."""
+ diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES]
+ coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS]
+ entities: list[DiffuserEntity] = []
+ for hublot, diffuser in diffusers.items():
+ coordinator = coordinators[hublot]
+ entities.append(DiffuserPerfumeAmount(diffuser, coordinator))
+
+ async_add_entities(entities)
+
+
+class DiffuserPerfumeAmount(DiffuserEntity, NumberEntity):
+ """Representation of a diffuser perfume amount number."""
+
+ _attr_icon = "mdi:gauge"
+ _attr_max_value = MAX_PERFUME_AMOUNT
+ _attr_min_value = MIN_PERFUME_AMOUNT
+
+ def __init__(
+ self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator
+ ) -> None:
+ """Initialize the diffuser perfume amount number."""
+ super().__init__(diffuser, coordinator, PERFUME_AMOUNT_SUFFIX)
+
+ @property
+ def value(self) -> int:
+ """Return the current perfume amount."""
+ return self._diffuser.perfume_amount
+
+ async def async_set_value(self, value: float) -> None:
+ """Set the perfume amount."""
+ if value.is_integer() and MIN_PERFUME_AMOUNT <= value <= MAX_PERFUME_AMOUNT:
+ await self._diffuser.set_perfume_amount(int(value))
+ else:
+ raise ValueError(
+ f"Can't set the perfume amount to {value}. "
+ f"Perfume amount must be an integer between {self.min_value} and {self.max_value}, inclusive"
+ )
diff --git a/homeassistant/components/rituals_perfume_genie/select.py b/homeassistant/components/rituals_perfume_genie/select.py
new file mode 100644
index 00000000000..ac6f4aa872a
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/select.py
@@ -0,0 +1,59 @@
+"""Support for Rituals Perfume Genie numbers."""
+from __future__ import annotations
+
+from pyrituals import Diffuser
+
+from homeassistant.components.select import SelectEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import AREA_SQUARE_METERS
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from . import RitualsDataUpdateCoordinator
+from .const import COORDINATORS, DEVICES, DOMAIN
+from .entity import DiffuserEntity
+
+ROOM_SIZE_SUFFIX = " Room Size"
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up the diffuser select entities."""
+ diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES]
+ coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS]
+ async_add_entities(
+ DiffuserRoomSize(diffuser, coordinators[hublot])
+ for hublot, diffuser in diffusers.items()
+ )
+
+
+class DiffuserRoomSize(DiffuserEntity, SelectEntity):
+ """Representation of a diffuser room size select entity."""
+
+ _attr_icon = "mdi:ruler-square"
+ _attr_unit_of_measurement = AREA_SQUARE_METERS
+ _attr_options = ["15", "30", "60", "100"]
+
+ def __init__(
+ self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator
+ ) -> None:
+ """Initialize the diffuser room size select entity."""
+ super().__init__(diffuser, coordinator, ROOM_SIZE_SUFFIX)
+ self._attr_entity_registry_enabled_default = diffuser.has_battery
+
+ @property
+ def current_option(self) -> str:
+ """Return the diffuser room size."""
+ return str(self._diffuser.room_size_square_meter)
+
+ async def async_select_option(self, option: str) -> None:
+ """Change the diffuser room size."""
+ if option in self.options:
+ await self._diffuser.set_room_size_square_meter(int(option))
+ else:
+ raise ValueError(
+ f"Can't set the room size to {option}. Allowed room sizes are: {self.options}"
+ )
diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py
index 31a04bb5b8f..2965371733b 100644
--- a/homeassistant/components/rituals_perfume_genie/sensor.py
+++ b/homeassistant/components/rituals_perfume_genie/sensor.py
@@ -13,12 +13,10 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import RitualsDataUpdateCoordinator
-from .const import COORDINATORS, DEVICES, DOMAIN, ID, SENSORS
+from .const import COORDINATORS, DEVICES, DOMAIN, SENSORS
from .entity import DiffuserEntity
-TITLE = "title"
-ICON = "icon"
-WIFI = "wific"
+ID = "id"
PERFUME = "rfidc"
FILL = "fillc"
@@ -30,8 +28,6 @@ PERFUME_SUFFIX = " Perfume"
FILL_SUFFIX = " Fill"
WIFI_SUFFIX = " Wifi"
-ATTR_SIGNAL_STRENGTH = "signal_strength"
-
async def async_setup_entry(
hass: HomeAssistant,
@@ -62,17 +58,14 @@ class DiffuserPerfumeSensor(DiffuserEntity):
"""Initialize the perfume sensor."""
super().__init__(diffuser, coordinator, PERFUME_SUFFIX)
- @property
- def icon(self) -> str:
- """Return the perfume sensor icon."""
- if self._diffuser.hub_data[SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID:
- return "mdi:tag-remove"
- return "mdi:tag-text"
+ self._attr_icon = "mdi:tag-text"
+ if diffuser.hub_data[SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID:
+ self._attr_icon = "mdi:tag-remove"
@property
def state(self) -> str:
"""Return the state of the perfume sensor."""
- return self._diffuser.hub_data[SENSORS][PERFUME][TITLE]
+ return self._diffuser.perfume
class DiffuserFillSensor(DiffuserEntity):
@@ -94,12 +87,15 @@ class DiffuserFillSensor(DiffuserEntity):
@property
def state(self) -> str:
"""Return the state of the fill sensor."""
- return self._diffuser.hub_data[SENSORS][FILL][TITLE]
+ return self._diffuser.fill
class DiffuserBatterySensor(DiffuserEntity):
"""Representation of a diffuser battery sensor."""
+ _attr_device_class = DEVICE_CLASS_BATTERY
+ _attr_unit_of_measurement = PERCENTAGE
+
def __init__(
self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator
) -> None:
@@ -111,20 +107,13 @@ class DiffuserBatterySensor(DiffuserEntity):
"""Return the state of the battery sensor."""
return self._diffuser.battery_percentage
- @property
- def device_class(self) -> str:
- """Return the class of the battery sensor."""
- return DEVICE_CLASS_BATTERY
-
- @property
- def unit_of_measurement(self) -> str:
- """Return the battery unit of measurement."""
- return PERCENTAGE
-
class DiffuserWifiSensor(DiffuserEntity):
"""Representation of a diffuser wifi sensor."""
+ _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH
+ _attr_unit_of_measurement = PERCENTAGE
+
def __init__(
self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator
) -> None:
@@ -135,13 +124,3 @@ class DiffuserWifiSensor(DiffuserEntity):
def state(self) -> int:
"""Return the state of the wifi sensor."""
return self._diffuser.wifi_percentage
-
- @property
- def device_class(self) -> str:
- """Return the class of the wifi sensor."""
- return DEVICE_CLASS_SIGNAL_STRENGTH
-
- @property
- def unit_of_measurement(self) -> str:
- """Return the wifi unit of measurement."""
- return PERCENTAGE
diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py
index a2ca89dc2ac..924a38dfde8 100644
--- a/homeassistant/components/rituals_perfume_genie/switch.py
+++ b/homeassistant/components/rituals_perfume_genie/switch.py
@@ -11,15 +11,9 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import RitualsDataUpdateCoordinator
-from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN
+from .const import COORDINATORS, DEVICES, DOMAIN
from .entity import DiffuserEntity
-FAN = "fanc"
-SPEED = "speedc"
-ROOM = "roomc"
-
-ON_STATE = "1"
-
async def async_setup_entry(
hass: HomeAssistant,
@@ -40,46 +34,37 @@ async def async_setup_entry(
class DiffuserSwitch(SwitchEntity, DiffuserEntity):
"""Representation of a diffuser switch."""
+ _attr_icon = "mdi:fan"
+
def __init__(
self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator
) -> None:
"""Initialize the diffuser switch."""
super().__init__(diffuser, coordinator, "")
- self._is_on = self._diffuser.is_on
-
- @property
- def icon(self) -> str:
- """Return the icon of the device."""
- return "mdi:fan"
+ self._attr_is_on = self._diffuser.is_on
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
- attributes = {
- "fan_speed": self._diffuser.hub_data[ATTRIBUTES][SPEED],
- "room_size": self._diffuser.hub_data[ATTRIBUTES][ROOM],
+ return {
+ "fan_speed": self._diffuser.perfume_amount,
+ "room_size": self._diffuser.room_size,
}
- return attributes
-
- @property
- def is_on(self) -> bool:
- """If the device is currently on or off."""
- return self._is_on
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
await self._diffuser.turn_on()
- self._is_on = True
+ self._attr_is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
await self._diffuser.turn_off()
- self._is_on = False
+ self._attr_is_on = False
self.async_write_ha_state()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
- self._is_on = self._diffuser.is_on
+ self._attr_is_on = self._diffuser.is_on
self.async_write_ha_state()
diff --git a/homeassistant/components/rituals_perfume_genie/translations/de.json b/homeassistant/components/rituals_perfume_genie/translations/de.json
index 67b8ed59e0b..72f18702457 100644
--- a/homeassistant/components/rituals_perfume_genie/translations/de.json
+++ b/homeassistant/components/rituals_perfume_genie/translations/de.json
@@ -13,7 +13,8 @@
"data": {
"email": "E-Mail",
"password": "Passwort"
- }
+ },
+ "title": "Verbinden Sie sich mit Ihrem Rituals-Konto"
}
}
}
diff --git a/homeassistant/components/rituals_perfume_genie/translations/he.json b/homeassistant/components/rituals_perfume_genie/translations/he.json
new file mode 100644
index 00000000000..ecb8a74bc6f
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/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"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u05d3\u05d5\u05d0\"\u05dc",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py
index e81f5260ac1..bc85915f39a 100644
--- a/homeassistant/components/roku/__init__.py
+++ b/homeassistant/components/roku/__init__.py
@@ -10,26 +10,14 @@ from rokuecp.models import Device
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_NAME, CONF_HOST
+from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.entity import DeviceInfo
-from homeassistant.helpers.update_coordinator import (
- CoordinatorEntity,
- DataUpdateCoordinator,
- UpdateFailed,
-)
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.dt import utcnow
-from .const import (
- ATTR_IDENTIFIERS,
- ATTR_MANUFACTURER,
- ATTR_MODEL,
- ATTR_SOFTWARE_VERSION,
- ATTR_SUGGESTED_AREA,
- DOMAIN,
-)
+from .const import DOMAIN
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
@@ -114,35 +102,3 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]):
return data
except RokuError as error:
raise UpdateFailed(f"Invalid response from API: {error}") from error
-
-
-class RokuEntity(CoordinatorEntity):
- """Defines a base Roku entity."""
-
- def __init__(
- self, *, device_id: str, name: str, coordinator: RokuDataUpdateCoordinator
- ) -> None:
- """Initialize the Roku entity."""
- super().__init__(coordinator)
- self._device_id = device_id
- self._name = name
-
- @property
- def name(self) -> str:
- """Return the name of the entity."""
- return self._name
-
- @property
- def device_info(self) -> DeviceInfo:
- """Return device information about this Roku device."""
- if self._device_id is None:
- return None
-
- return {
- ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)},
- ATTR_NAME: self.name,
- ATTR_MANUFACTURER: self.coordinator.data.info.brand,
- ATTR_MODEL: self.coordinator.data.info.model_name,
- ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version,
- ATTR_SUGGESTED_AREA: self.coordinator.data.info.device_location,
- }
diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py
new file mode 100644
index 00000000000..aefc335e64d
--- /dev/null
+++ b/homeassistant/components/roku/entity.py
@@ -0,0 +1,42 @@
+"""Base Entity for Roku."""
+from __future__ import annotations
+
+from homeassistant.const import ATTR_NAME
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from . import RokuDataUpdateCoordinator
+from .const import (
+ ATTR_IDENTIFIERS,
+ ATTR_MANUFACTURER,
+ ATTR_MODEL,
+ ATTR_SOFTWARE_VERSION,
+ ATTR_SUGGESTED_AREA,
+ DOMAIN,
+)
+
+
+class RokuEntity(CoordinatorEntity):
+ """Defines a base Roku entity."""
+
+ def __init__(
+ self, *, device_id: str, coordinator: RokuDataUpdateCoordinator
+ ) -> None:
+ """Initialize the Roku entity."""
+ super().__init__(coordinator)
+ self._device_id = device_id
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device information about this Roku device."""
+ if self._device_id is None:
+ return None
+
+ return {
+ ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)},
+ ATTR_NAME: self.name,
+ ATTR_MANUFACTURER: self.coordinator.data.info.brand,
+ ATTR_MODEL: self.coordinator.data.info.model_name,
+ ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version,
+ ATTR_SUGGESTED_AREA: self.coordinator.data.info.device_location,
+ }
diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py
index ce5a77f06f6..dc0f2ff704c 100644
--- a/homeassistant/components/roku/media_player.py
+++ b/homeassistant/components/roku/media_player.py
@@ -37,9 +37,10 @@ from homeassistant.const import (
from homeassistant.helpers import entity_platform
from homeassistant.helpers.network import is_internal_request
-from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler
+from . import RokuDataUpdateCoordinator, roku_exception_handler
from .browse_media import build_item_response, library_payload
from .const import ATTR_KEYWORD, DOMAIN, SERVICE_SEARCH
+from .entity import RokuEntity
_LOGGER = logging.getLogger(__name__)
@@ -82,11 +83,11 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
"""Initialize the Roku device."""
super().__init__(
coordinator=coordinator,
- name=coordinator.data.info.name,
device_id=unique_id,
)
- self._unique_id = unique_id
+ self._attr_name = coordinator.data.info.name
+ self._attr_unique_id = unique_id
def _media_playback_trackable(self) -> bool:
"""Detect if we have enough media data to track playback."""
@@ -95,11 +96,6 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
return self.coordinator.data.media.duration > 0
- @property
- def unique_id(self) -> str:
- """Return the unique ID for this entity."""
- return self._unique_id
-
@property
def device_class(self) -> str | None:
"""Return the class of this device."""
diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py
index 7eb8396d6fa..28095311d81 100644
--- a/homeassistant/components/roku/remote.py
+++ b/homeassistant/components/roku/remote.py
@@ -6,8 +6,9 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler
+from . import RokuDataUpdateCoordinator, roku_exception_handler
from .const import DOMAIN
+from .entity import RokuEntity
async def async_setup_entry(
@@ -28,16 +29,11 @@ class RokuRemote(RokuEntity, RemoteEntity):
"""Initialize the Roku device."""
super().__init__(
device_id=unique_id,
- name=coordinator.data.info.name,
coordinator=coordinator,
)
- self._unique_id = unique_id
-
- @property
- def unique_id(self) -> str:
- """Return the unique ID for this entity."""
- return self._unique_id
+ self._attr_name = coordinator.data.info.name
+ self._attr_unique_id = unique_id
@property
def is_on(self) -> bool:
diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json
index 235cf4ad159..68cbe528e87 100644
--- a/homeassistant/components/roku/strings.json
+++ b/homeassistant/components/roku/strings.json
@@ -10,8 +10,7 @@
},
"discovery_confirm": {
"title": "Roku",
- "description": "Do you want to set up {name}?",
- "data": {}
+ "description": "Do you want to set up {name}?"
}
},
"error": {
diff --git a/homeassistant/components/roku/translations/de.json b/homeassistant/components/roku/translations/de.json
index 152161cb27f..77cd1b94b6e 100644
--- a/homeassistant/components/roku/translations/de.json
+++ b/homeassistant/components/roku/translations/de.json
@@ -8,7 +8,7 @@
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
},
- "flow_title": "Roku: {name}",
+ "flow_title": "{name}",
"step": {
"discovery_confirm": {
"description": "M\u00f6chtest du {name} einrichten?",
diff --git a/homeassistant/components/roku/translations/he.json b/homeassistant/components/roku/translations/he.json
new file mode 100644
index 00000000000..41d59c29fd8
--- /dev/null
+++ b/homeassistant/components/roku/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": "{name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json
index 5d833960240..8c2d9d4e84a 100644
--- a/homeassistant/components/roku/translations/it.json
+++ b/homeassistant/components/roku/translations/it.json
@@ -11,10 +11,18 @@
"flow_title": "{name}",
"step": {
"discovery_confirm": {
+ "data": {
+ "one": "Pi\u00f9",
+ "other": "Altri"
+ },
"description": "Vuoi configurare {name}?",
"title": "Roku"
},
"ssdp_confirm": {
+ "data": {
+ "one": "Pi\u00f9",
+ "other": "Altri"
+ },
"description": "Vuoi impostare {name}?",
"title": "Roku"
},
diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py
index c3ccd051dd8..4fdcbceab07 100644
--- a/homeassistant/components/roomba/config_flow.py
+++ b/homeassistant/components/roomba/config_flow.py
@@ -206,7 +206,7 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
password = await self.hass.async_add_executor_job(roomba_pw.get_password)
- except (OSError, ConnectionRefusedError):
+ except OSError:
return await self.async_step_link_manual()
if not password:
diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py
index 45a69d38576..2f57ef954b6 100644
--- a/homeassistant/components/roomba/irobot_base.py
+++ b/homeassistant/components/roomba/irobot_base.py
@@ -1,4 +1,6 @@
"""Base class for iRobot devices."""
+from __future__ import annotations
+
import asyncio
import logging
@@ -23,6 +25,7 @@ from homeassistant.components.vacuum import (
)
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.entity import Entity
+import homeassistant.util.dt as dt_util
from . import roomba_reported_state
from .const import DOMAIN
@@ -191,14 +194,10 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity):
# currently on
if self.state == STATE_CLEANING:
# Get clean mission status
- mission_state = state.get("cleanMissionStatus", {})
- cleaning_time = mission_state.get("mssnM")
- cleaned_area = mission_state.get("sqft") # Imperial
- # Convert to m2 if the unit_system is set to metric
- if cleaned_area and self.hass.config.units.is_metric:
- cleaned_area = round(cleaned_area * 0.0929)
- state_attrs[ATTR_CLEANING_TIME] = cleaning_time
- state_attrs[ATTR_CLEANED_AREA] = cleaned_area
+ (
+ state_attrs[ATTR_CLEANING_TIME],
+ state_attrs[ATTR_CLEANED_AREA],
+ ) = self.get_cleaning_status(state)
# Error
if self.vacuum.error_code != 0:
@@ -219,6 +218,25 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity):
return state_attrs
+ def get_cleaning_status(self, state) -> tuple[int, int]:
+ """Return the cleaning time and cleaned area from the device."""
+ if not (mission_state := state.get("cleanMissionStatus")):
+ return (0, 0)
+
+ if cleaning_time := mission_state.get("mssnM", 0):
+ pass
+ elif start_time := mission_state.get("mssnStrtTm"):
+ now = dt_util.as_timestamp(dt_util.utcnow())
+ if now > start_time:
+ cleaning_time = (now - start_time) // 60
+
+ if cleaned_area := mission_state.get("sqft", 0): # Imperial
+ # Convert to m2 if the unit_system is set to metric
+ if self.hass.config.units.is_metric:
+ cleaned_area = round(cleaned_area * 0.0929)
+
+ return (cleaning_time, cleaned_area)
+
def on_message(self, json_data):
"""Update state on message change."""
state = json_data.get("state", {}).get("reported", {})
diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json
index 867d2bf633f..1a37745302a 100644
--- a/homeassistant/components/roomba/strings.json
+++ b/homeassistant/components/roomba/strings.json
@@ -2,7 +2,7 @@
"config": {
"flow_title": "{name} ({host})",
"step": {
- "init": {
+ "user": {
"title": "Automatically connect to the device",
"description": "Select a Roomba or Braava.",
"data": {
@@ -16,7 +16,7 @@
"host": "[%key:common::config_flow::data::host%]",
"blid": "BLID"
}
- },
+ },
"link": {
"title": "Retrieve Password",
"description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds."
@@ -27,7 +27,7 @@
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
- }
+ }
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
@@ -37,7 +37,7 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"not_irobot_device": "Discovered device is not an iRobot device",
"short_blid": "The BLID was truncated"
- }
+ }
},
"options": {
"step": {
diff --git a/homeassistant/components/roomba/translations/ca.json b/homeassistant/components/roomba/translations/ca.json
index d41b7d3833f..f237967b8a4 100644
--- a/homeassistant/components/roomba/translations/ca.json
+++ b/homeassistant/components/roomba/translations/ca.json
@@ -45,8 +45,8 @@
"host": "Amfitri\u00f3",
"password": "Contrasenya"
},
- "description": "Actualment la recuperaci\u00f3 de BLID i la contrasenya \u00e9s un proc\u00e9s manual. Segueix els passos de la documentaci\u00f3 a: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
- "title": "Connexi\u00f3 amb el dispositiu"
+ "description": "Selecciona un/a Roomba o Braava.",
+ "title": "Connexi\u00f3 autom\u00e0tica amb el dispositiu"
}
}
},
diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json
index b66a6681be7..193469008e2 100644
--- a/homeassistant/components/roomba/translations/de.json
+++ b/homeassistant/components/roomba/translations/de.json
@@ -9,7 +9,7 @@
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
},
- "flow_title": "iRobot {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"init": {
"data": {
@@ -19,7 +19,7 @@
"title": "Automatisch mit dem Ger\u00e4t verbinden"
},
"link": {
- "description": "Halte die Home-Taste von {name} gedr\u00fcckt, bis das Ger\u00e4t einen Ton erzeugt (ca. zwei Sekunden).",
+ "description": "Halte die Home-Taste von {name} gedr\u00fcckt, bis das Ger\u00e4t einen Ton erzeugt (ca. zwei Sekunden) und sende die Best\u00e4tigung innerhalb von 30 Sekunden ab.",
"title": "Passwort abrufen"
},
"link_manual": {
@@ -34,6 +34,7 @@
"blid": "BLID",
"host": "Host"
},
+ "description": "Es wurde kein Roomba oder Braava in Ihrem Netzwerk entdeckt. Die BLID ist der Teil des Ger\u00e4te-Hostnamens nach `iRobot-` oder `Roomba-`. Bitte folgen Sie den Schritten, die in der Dokumentation unter: {auth_help_url}",
"title": "Manuell mit dem Ger\u00e4t verbinden"
},
"user": {
@@ -44,8 +45,8 @@
"host": "Host",
"password": "Passwort"
},
- "description": "Das Abrufen der BLID und des Kennworts erfolgt manuell. Befolgen Sie die in der Dokumentation beschriebenen Schritte unter: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
- "title": "Stellen Sie eine Verbindung zum Ger\u00e4t her"
+ "description": "W\u00e4hlen Sie einen Roomba oder Braava aus.",
+ "title": "Automatisch mit dem Ger\u00e4t verbinden"
}
}
},
diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json
index 32853564e53..df95782f52f 100644
--- a/homeassistant/components/roomba/translations/en.json
+++ b/homeassistant/components/roomba/translations/en.json
@@ -45,8 +45,8 @@
"host": "Host",
"password": "Password"
},
- "description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
- "title": "Connect to the device"
+ "description": "Select a Roomba or Braava.",
+ "title": "Automatically connect to the device"
}
}
},
diff --git a/homeassistant/components/roomba/translations/et.json b/homeassistant/components/roomba/translations/et.json
index 0f992a57de6..7a8f33ebf57 100644
--- a/homeassistant/components/roomba/translations/et.json
+++ b/homeassistant/components/roomba/translations/et.json
@@ -45,8 +45,8 @@
"host": "",
"password": "Salas\u00f5na"
},
- "description": "Praegu on BLID ja parooli toomine k\u00e4sitsi protsess. J\u00e4rgi dokumentatsioonis toodud juhiseid aadressil: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
- "title": "\u00dchendu seadmega"
+ "description": "Vali Roomba v\u00f5i Braava seade",
+ "title": "\u00dchenda seadmega automaatselt"
}
}
},
diff --git a/homeassistant/components/roomba/translations/he.json b/homeassistant/components/roomba/translations/he.json
index 3007c0e968c..e2d0c48b0b9 100644
--- a/homeassistant/components/roomba/translations/he.json
+++ b/homeassistant/components/roomba/translations/he.json
@@ -1,10 +1,40 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{name} ({host})",
"step": {
- "user": {
+ "init": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ },
+ "link": {
+ "title": "\u05d0\u05d7\u05d6\u05e8 \u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "link_manual": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "description": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05de\u05d4\u05d4\u05ea\u05e7\u05df \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9. \u05e0\u05d0 \u05d1\u05e6\u05e2 \u05d0\u05ea \u05d4\u05e9\u05dc\u05d1\u05d9\u05dd \u05d4\u05de\u05ea\u05d5\u05d0\u05e8\u05d9\u05dd \u05d1\u05ea\u05d9\u05e2\u05d5\u05d3 \u05d1\u05db\u05ea\u05d5\u05d1\u05ea: {auth_help_url}",
+ "title": "\u05d4\u05d6\u05df \u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "manual": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
}
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "description": "\u05db\u05e8\u05d2\u05e2 \u05d0\u05d7\u05d6\u05d5\u05e8 \u05d4-BLID \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d5\u05d0 \u05ea\u05d4\u05dc\u05d9\u05da \u05d9\u05d3\u05e0\u05d9. \u05e0\u05d0 \u05d1\u05e6\u05e2 \u05d0\u05ea \u05d4\u05e9\u05dc\u05d1\u05d9\u05dd \u05d4\u05de\u05ea\u05d5\u05d0\u05e8\u05d9\u05dd \u05d1\u05ea\u05d9\u05e2\u05d5\u05d3 \u05d1\u05db\u05ea\u05d5\u05d1\u05ea: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials"
}
}
}
diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json
index 931671f92d2..51957ba8847 100644
--- a/homeassistant/components/roomba/translations/hu.json
+++ b/homeassistant/components/roomba/translations/hu.json
@@ -9,7 +9,7 @@
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
- "flow_title": "iRobot {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"init": {
"data": {
diff --git a/homeassistant/components/roomba/translations/it.json b/homeassistant/components/roomba/translations/it.json
index 0b2c9079ac9..d5909d5bcc5 100644
--- a/homeassistant/components/roomba/translations/it.json
+++ b/homeassistant/components/roomba/translations/it.json
@@ -45,8 +45,8 @@
"host": "Host",
"password": "Password"
},
- "description": "Attualmente il recupero del BLID e della password \u00e8 un processo manuale. Si prega di seguire i passi descritti nella documentazione all'indirizzo: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
- "title": "Connettersi al dispositivo"
+ "description": "Seleziona un Roomba o un Braava.",
+ "title": "Connetti automaticamente al dispositivo"
}
}
},
diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json
index a18bd89ae12..40c821b62db 100644
--- a/homeassistant/components/roomba/translations/nl.json
+++ b/homeassistant/components/roomba/translations/nl.json
@@ -45,8 +45,8 @@
"host": "Host",
"password": "Wachtwoord"
},
- "description": "Het ophalen van de BLID en het wachtwoord is momenteel een handmatig proces. Volg de stappen in de documentatie op: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
- "title": "Verbinding maken met het apparaat"
+ "description": "Kies een Roomba of Braava.",
+ "title": "Automatisch verbinding maken met het apparaat"
}
}
},
diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json
index 2caba79f50c..1cacffdf425 100644
--- a/homeassistant/components/roomba/translations/no.json
+++ b/homeassistant/components/roomba/translations/no.json
@@ -45,8 +45,8 @@
"host": "Vert",
"password": "Passord"
},
- "description": "Henting av BLID og passord er en manuell prosess. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
- "title": "Koble til enheten"
+ "description": "Velg en Roomba eller Braava.",
+ "title": "Koble automatisk til enheten"
}
}
},
diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json
index d4624a91906..118d5a8ece7 100644
--- a/homeassistant/components/roomba/translations/pl.json
+++ b/homeassistant/components/roomba/translations/pl.json
@@ -45,8 +45,8 @@
"host": "Nazwa hosta lub adres IP",
"password": "Has\u0142o"
},
- "description": "Obecnie pobieranie BLID i has\u0142a jest procesem r\u0119cznym. Prosz\u0119 post\u0119powa\u0107 zgodnie z instrukcjami zawartymi w dokumentacji pod adresem: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials.",
- "title": "Po\u0142\u0105czenie z urz\u0105dzeniem"
+ "description": "Wybierz Roomb\u0119 lub Braava",
+ "title": "Po\u0142\u0105cz si\u0119 automatycznie z urz\u0105dzeniem"
}
}
},
diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json
index 6ff4feb4e9c..f61ecde08ec 100644
--- a/homeassistant/components/roomba/translations/ru.json
+++ b/homeassistant/components/roomba/translations/ru.json
@@ -45,8 +45,8 @@
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c"
},
- "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c BLID \u0438 \u043f\u0430\u0440\u043e\u043b\u044c:\nhttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
- "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u044b\u043b\u0435\u0441\u043e\u0441 \u0438\u0437 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 Roomba \u0438\u043b\u0438 Braava.",
+ "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
}
}
},
diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json
index 81ba19a3a57..4a5891d896e 100644
--- a/homeassistant/components/roomba/translations/zh-Hant.json
+++ b/homeassistant/components/roomba/translations/zh-Hant.json
@@ -45,8 +45,8 @@
"host": "\u4e3b\u6a5f\u7aef",
"password": "\u5bc6\u78bc"
},
- "description": "\u76ee\u524d\u63a5\u6536 BLID \u8207\u5bc6\u78bc\u70ba\u624b\u52d5\u904e\u7a0b\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1ahttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
- "title": "\u9023\u7dda\u81f3\u88dd\u7f6e"
+ "description": "\u9078\u64c7 Roomba \u6216 Braava\u3002",
+ "title": "\u81ea\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e"
}
}
},
diff --git a/homeassistant/components/roon/config_flow.py b/homeassistant/components/roon/config_flow.py
index 799d50bdaab..31391a0ff36 100644
--- a/homeassistant/components/roon/config_flow.py
+++ b/homeassistant/components/roon/config_flow.py
@@ -18,7 +18,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
-DATA_SCHEMA = vol.Schema({"host": str})
+DATA_SCHEMA = vol.Schema({vol.Required("host"): str})
TIMEOUT = 120
diff --git a/homeassistant/components/roon/translations/he.json b/homeassistant/components/roon/translations/he.json
new file mode 100644
index 00000000000..9d3230d257e
--- /dev/null
+++ b/homeassistant/components/roon/translations/he.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "link": {
+ "description": "\u05e2\u05dc\u05d9\u05da \u05dc\u05d0\u05e9\u05e8 \u05dc-Home Assistant \u05d1-Roon. \u05dc\u05d0\u05d7\u05e8 \u05e9\u05ea\u05dc\u05d7\u05e5 \u05e2\u05dc \u05e9\u05dc\u05d7, \u05e2\u05d1\u05d5\u05e8 \u05dc\u05d9\u05d9\u05e9\u05d5\u05dd Roon Core, \u05e4\u05ea\u05d7 \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d5\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea HomeAssistant \u05d1\u05db\u05e8\u05d8\u05d9\u05e1\u05d9\u05d9\u05d4 \u05d4\u05e8\u05d7\u05d1\u05d5\u05ea."
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ },
+ "description": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d2\u05dc\u05d5\u05ea \u05d0\u05ea \u05e9\u05e8\u05ea Roon, \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d4-IP."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py
index 305ad7d1f62..eeb7c4fe181 100644
--- a/homeassistant/components/rpi_power/__init__.py
+++ b/homeassistant/components/rpi_power/__init__.py
@@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant
PLATFORMS = ["binary_sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Raspberry Pi Power Supply Checker from a config entry."""
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
diff --git a/homeassistant/components/rpi_power/translations/he.json b/homeassistant/components/rpi_power/translations/he.json
new file mode 100644
index 00000000000..a18f311e43a
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "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": {
+ "confirm": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py
index 4c02f49d86a..c750c7aa83c 100644
--- a/homeassistant/components/rtorrent/sensor.py
+++ b/homeassistant/components/rtorrent/sensor.py
@@ -122,7 +122,7 @@ class RTorrentSensor(SensorEntity):
try:
self.data = multicall()
self._available = True
- except (xmlrpc.client.ProtocolError, ConnectionRefusedError, OSError) as ex:
+ except (xmlrpc.client.ProtocolError, OSError) as ex:
_LOGGER.error("Connection to rtorrent failed (%s)", ex)
self._available = False
return
diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py
index 463c7b1d550..7d34e620a13 100644
--- a/homeassistant/components/ruckus_unleashed/config_flow.py
+++ b/homeassistant/components/ruckus_unleashed/config_flow.py
@@ -12,7 +12,13 @@ from .const import API_SERIAL, API_SYSTEM_OVERVIEW, DOMAIN
_LOGGER = logging.getLogger(__package__)
-DATA_SCHEMA = vol.Schema({"host": str, "username": str, "password": str})
+DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required("host"): str,
+ vol.Required("username"): str,
+ vol.Required("password"): str,
+ }
+)
def validate_input(hass: core.HomeAssistant, data):
diff --git a/homeassistant/components/ruckus_unleashed/translations/he.json b/homeassistant/components/ruckus_unleashed/translations/he.json
index 6ef580c7d8d..479d2f2f5e8 100644
--- a/homeassistant/components/ruckus_unleashed/translations/he.json
+++ b/homeassistant/components/ruckus_unleashed/translations/he.json
@@ -1,6 +1,11 @@
{
"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",
+ "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": {
diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py
index f1def71cc64..fb3b31764f8 100644
--- a/homeassistant/components/saj/sensor.py
+++ b/homeassistant/components/saj/sensor.py
@@ -1,11 +1,17 @@
"""SAJ solar inverter interface."""
+from __future__ import annotations
+
from datetime import date
import logging
import pysaj
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.components.sensor import (
+ PLATFORM_SCHEMA,
+ STATE_CLASS_MEASUREMENT,
+ SensorEntity,
+)
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
@@ -27,6 +33,7 @@ from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_call_later
+from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -169,6 +176,11 @@ class SAJsensor(SensorEntity):
self._serialnumber = serialnumber
self._state = self._sensor.value
+ if pysaj_sensor.name in ("current_power", "total_yield", "temperature"):
+ self._attr_state_class = STATE_CLASS_MEASUREMENT
+ if pysaj_sensor.name == "total_yield":
+ self._attr_last_reset = dt_util.utc_from_timestamp(0)
+
@property
def name(self):
"""Return the name of the sensor."""
diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py
index 31b666793af..09b513c3830 100644
--- a/homeassistant/components/samsungtv/__init__.py
+++ b/homeassistant/components/samsungtv/__init__.py
@@ -5,8 +5,10 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN
+from homeassistant.config_entries import ConfigEntryNotReady
from homeassistant.const import (
CONF_HOST,
+ CONF_MAC,
CONF_METHOD,
CONF_NAME,
CONF_PORT,
@@ -16,8 +18,16 @@ from homeassistant.const import (
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from .bridge import SamsungTVBridge
-from .const import CONF_ON_ACTION, DEFAULT_NAME, DOMAIN, LOGGER
+from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info
+from .const import (
+ CONF_ON_ACTION,
+ DEFAULT_NAME,
+ DOMAIN,
+ LEGACY_PORT,
+ LOGGER,
+ METHOD_LEGACY,
+ METHOD_WEBSOCKET,
+)
def ensure_unique_hosts(value):
@@ -90,13 +100,7 @@ async def async_setup_entry(hass, entry):
"""Set up the Samsung TV platform."""
# Initialize bridge
- data = entry.data.copy()
- bridge = _async_get_device_bridge(data)
- if bridge.port is None and bridge.default_port is not None:
- # For backward compat, set default port for websocket tv
- data[CONF_PORT] = bridge.default_port
- hass.config_entries.async_update_entry(entry, data=data)
- bridge = _async_get_device_bridge(data)
+ bridge = await _async_create_bridge_with_updated_data(hass, entry)
def stop_bridge(event):
"""Stop SamsungTV bridge connection."""
@@ -111,6 +115,46 @@ async def async_setup_entry(hass, entry):
return True
+async def _async_create_bridge_with_updated_data(hass, entry):
+ """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
+
+ if not port or not method:
+ 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:
+ raise ConfigEntryNotReady(
+ "Failed to determine connection method, make sure the device is on."
+ )
+
+ updated_data[CONF_PORT] = port
+ updated_data[CONF_METHOD] = method
+
+ bridge = _async_get_device_bridge({**entry.data, **updated_data})
+
+ if not entry.data.get(CONF_MAC) and bridge.method == METHOD_WEBSOCKET:
+ if info:
+ mac = mac_from_device_info(info)
+ else:
+ mac = await hass.async_add_executor_job(bridge.mac_from_device)
+ if mac:
+ updated_data[CONF_MAC] = mac
+
+ if updated_data:
+ data = {**entry.data, **updated_data}
+ hass.config_entries.async_update_entry(entry, data=data)
+
+ return bridge
+
+
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py
index 7e1c24c6d2f..1cdd63acd3c 100644
--- a/homeassistant/components/samsungtv/bridge.py
+++ b/homeassistant/components/samsungtv/bridge.py
@@ -17,11 +17,14 @@ from homeassistant.const import (
CONF_TIMEOUT,
CONF_TOKEN,
)
+from homeassistant.helpers.device_registry import format_mac
from .const import (
CONF_DESCRIPTION,
+ LEGACY_PORT,
LOGGER,
METHOD_LEGACY,
+ METHOD_WEBSOCKET,
RESULT_AUTH_MISSING,
RESULT_CANNOT_CONNECT,
RESULT_NOT_SUPPORTED,
@@ -34,13 +37,44 @@ from .const import (
)
+def mac_from_device_info(info):
+ """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"])
+ return None
+
+
+async def async_get_device_info(hass, bridge, host):
+ """Fetch the port, method, and device info."""
+ return await hass.async_add_executor_job(_get_device_info, bridge, host)
+
+
+def _get_device_info(bridge, host):
+ """Fetch the port, method, and device info."""
+ if bridge and bridge.port:
+ return bridge.port, bridge.method, bridge.device_info()
+
+ for port in WEBSOCKET_PORTS:
+ bridge = SamsungTVBridge.get_bridge(METHOD_WEBSOCKET, host, port)
+ if info := bridge.device_info():
+ return port, METHOD_WEBSOCKET, info
+
+ bridge = SamsungTVBridge.get_bridge(METHOD_LEGACY, host, LEGACY_PORT)
+ result = bridge.try_connect()
+ if result in (RESULT_SUCCESS, RESULT_AUTH_MISSING):
+ return LEGACY_PORT, METHOD_LEGACY, None
+
+ return None, None, None
+
+
class SamsungTVBridge(ABC):
"""The Base Bridge abstract class."""
@staticmethod
def get_bridge(method, host, port=None, token=None):
"""Get Bridge instance."""
- if method == METHOD_LEGACY:
+ if method == METHOD_LEGACY or port == LEGACY_PORT:
return SamsungTVLegacyBridge(method, host, port)
return SamsungTVWSBridge(method, host, port, token)
@@ -50,7 +84,6 @@ class SamsungTVBridge(ABC):
self.method = method
self.host = host
self.token = None
- self.default_port = None
self._remote = None
self._callback = None
@@ -66,6 +99,10 @@ class SamsungTVBridge(ABC):
def device_info(self):
"""Try to gather infos of this TV."""
+ @abstractmethod
+ def mac_from_device(self):
+ """Try to fetch the mac address of the TV."""
+
def is_on(self):
"""Tells if the TV is on."""
if self._remote:
@@ -137,7 +174,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge):
def __init__(self, method, host, port):
"""Initialize Bridge."""
- super().__init__(method, host, None)
+ super().__init__(method, host, LEGACY_PORT)
self.config = {
CONF_NAME: VALUE_CONF_NAME,
CONF_DESCRIPTION: VALUE_CONF_NAME,
@@ -148,6 +185,10 @@ class SamsungTVLegacyBridge(SamsungTVBridge):
CONF_TIMEOUT: 1,
}
+ def mac_from_device(self):
+ """Try to fetch the mac address of the TV."""
+ return None
+
def try_connect(self):
"""Try to connect to the Legacy TV."""
config = {
@@ -193,6 +234,8 @@ class SamsungTVLegacyBridge(SamsungTVBridge):
except AccessDenied:
self._notify_callback()
raise
+ except (ConnectionClosed, OSError):
+ pass
return self._remote
def _send_key(self, key):
@@ -212,7 +255,11 @@ class SamsungTVWSBridge(SamsungTVBridge):
"""Initialize Bridge."""
super().__init__(method, host, port)
self.token = token
- self.default_port = 8001
+
+ def mac_from_device(self):
+ """Try to fetch the mac address of the TV."""
+ info = self.device_info()
+ return mac_from_device_info(info) if info else None
def try_connect(self):
"""Try to connect to the Websocket TV."""
diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py
index e29298da2eb..76128a1f1dd 100644
--- a/homeassistant/components/samsungtv/config_flow.py
+++ b/homeassistant/components/samsungtv/config_flow.py
@@ -24,7 +24,7 @@ from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.typing import DiscoveryInfoType
-from .bridge import SamsungTVBridge
+from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info
from .const import (
ATTR_PROPERTIES,
CONF_MANUFACTURER,
@@ -47,27 +47,15 @@ DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME):
SUPPORTED_METHODS = [METHOD_LEGACY, METHOD_WEBSOCKET]
-def _get_device_info(host):
- """Fetch device info by any websocket method."""
- for port in WEBSOCKET_PORTS:
- bridge = SamsungTVBridge.get_bridge(METHOD_WEBSOCKET, host, port)
- if info := bridge.device_info():
- return info
- return None
-
-
-async def async_get_device_info(hass, bridge, host):
- """Fetch device info from bridge or websocket."""
- if bridge:
- return await hass.async_add_executor_job(bridge.device_info)
-
- return await hass.async_add_executor_job(_get_device_info, host)
-
-
def _strip_uuid(udn):
return udn[5:] if udn.startswith("uuid:") else udn
+def _entry_is_complete(entry):
+ """Return True if the config entry information is complete."""
+ return bool(entry.unique_id and entry.data.get(CONF_MAC))
+
+
class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Samsung TV config flow."""
@@ -107,14 +95,22 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _async_set_device_unique_id(self, raise_on_progress=True):
"""Set device unique_id."""
- await self._async_get_and_check_device_info()
+ if not await self._async_get_and_check_device_info():
+ raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED)
await self._async_set_unique_id_from_udn(raise_on_progress)
+ self._async_update_and_abort_for_matching_unique_id()
async def _async_set_unique_id_from_udn(self, raise_on_progress=True):
"""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)
- self._async_update_existing_host_entry(self._host)
+ if (entry := self._async_update_existing_host_entry()) and _entry_is_complete(
+ entry
+ ):
+ raise data_entry_flow.AbortFlow("already_configured")
+
+ def _async_update_and_abort_for_matching_unique_id(self):
+ """Abort and update host and mac if we have it."""
updates = {CONF_HOST: self._host}
if self._mac:
updates[CONF_MAC] = self._mac
@@ -134,9 +130,11 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _async_get_and_check_device_info(self):
"""Try to get the device info."""
- info = await async_get_device_info(self.hass, self._bridge, self._host)
+ _port, _method, info = await async_get_device_info(
+ self.hass, self._bridge, self._host
+ )
if not info:
- raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED)
+ return False
dev_info = info.get("device", {})
device_type = dev_info.get("type")
if device_type != "Samsung SmartTV":
@@ -146,9 +144,10 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._name = name.replace("[TV] ", "") if name else device_type
self._title = f"{self._name} ({self._model})"
self._udn = _strip_uuid(dev_info.get("udn", info["id"]))
- if dev_info.get("networkType") == "wireless" and dev_info.get("wifiMac"):
- self._mac = format_mac(dev_info.get("wifiMac"))
+ if mac := mac_from_device_info(info):
+ self._mac = mac
self._device_info = info
+ return True
async def async_step_import(self, user_input=None):
"""Handle configuration by yaml file."""
@@ -156,11 +155,11 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# since the TV may be off at startup
await self._async_set_name_host_from_input(user_input)
self._async_abort_entries_match({CONF_HOST: self._host})
- if user_input.get(CONF_PORT) in WEBSOCKET_PORTS:
+ port = user_input.get(CONF_PORT)
+ if port in WEBSOCKET_PORTS:
user_input[CONF_METHOD] = METHOD_WEBSOCKET
- else:
+ elif port == LEGACY_PORT:
user_input[CONF_METHOD] = METHOD_LEGACY
- user_input[CONF_PORT] = LEGACY_PORT
user_input[CONF_MANUFACTURER] = DEFAULT_MANUFACTURER
return self.async_create_entry(
title=self._title,
@@ -191,52 +190,65 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
@callback
- def _async_update_existing_host_entry(self, host):
+ def _async_update_existing_host_entry(self):
+ """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] != host:
+ if entry.data[CONF_HOST] != self._host:
continue
entry_kw_args = {}
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):
- data_copy = dict(entry.data)
- data_copy[CONF_MAC] = self._mac
- entry_kw_args["data"] = data_copy
+ 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)
- return entry
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(entry.entry_id)
+ )
+ return entry
return None
- async def _async_start_discovery(self):
+ async def _async_start_discovery_with_mac_address(self):
"""Start discovery."""
assert self._host is not None
- if entry := self._async_update_existing_host_entry(self._host):
- if entry.unique_id:
- # Let the flow continue to fill the missing
- # unique id as we may be able to obtain it
- # in the next step
- raise data_entry_flow.AbortFlow("already_configured")
+ if (entry := self._async_update_existing_host_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")
+ self._async_abort_if_host_already_in_progress()
+ @callback
+ def _async_abort_if_host_already_in_progress(self):
self.context[CONF_HOST] = self._host
for progress in self._async_in_progress():
if progress.get("context", {}).get(CONF_HOST) == self._host:
raise data_entry_flow.AbortFlow("already_in_progress")
- async def async_step_ssdp(self, discovery_info: DiscoveryInfoType):
- """Handle a flow initialized by ssdp discovery."""
- LOGGER.debug("Samsung device found via SSDP: %s", discovery_info)
- self._udn = _strip_uuid(discovery_info[ATTR_UPNP_UDN])
- self._host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
- await self._async_set_unique_id_from_udn()
- await self._async_start_discovery()
- self._manufacturer = discovery_info[ATTR_UPNP_MANUFACTURER]
+ @callback
+ def _abort_if_manufacturer_is_not_samsung(self):
if not self._manufacturer or not self._manufacturer.lower().startswith(
"samsung"
):
raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED)
- self._name = self._title = self._model = discovery_info.get(
- ATTR_UPNP_MODEL_NAME
- )
+
+ async def async_step_ssdp(self, discovery_info: DiscoveryInfoType):
+ """Handle a flow initialized by ssdp discovery."""
+ LOGGER.debug("Samsung device found via SSDP: %s", discovery_info)
+ model_name = discovery_info.get(ATTR_UPNP_MODEL_NAME)
+ self._udn = _strip_uuid(discovery_info[ATTR_UPNP_UDN])
+ self._host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
+ await self._async_set_unique_id_from_udn()
+ self._manufacturer = discovery_info[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
+ self._async_update_and_abort_for_matching_unique_id()
+ self._async_abort_if_host_already_in_progress()
self.context["title_placeholders"] = {"device": self._title}
return await self.async_step_confirm()
@@ -245,7 +257,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
LOGGER.debug("Samsung device found via DHCP: %s", discovery_info)
self._mac = discovery_info[MAC_ADDRESS]
self._host = discovery_info[IP_ADDRESS]
- await self._async_start_discovery()
+ await self._async_start_discovery_with_mac_address()
await self._async_set_device_unique_id()
self.context["title_placeholders"] = {"device": self._title}
return await self.async_step_confirm()
@@ -255,7 +267,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info)
self._mac = format_mac(discovery_info[ATTR_PROPERTIES]["deviceid"])
self._host = discovery_info[CONF_HOST]
- await self._async_start_discovery()
+ await self._async_start_discovery_with_mac_address()
await self._async_set_device_unique_id()
self.context["title_placeholders"] = {"device": self._title}
return await self.async_step_confirm()
diff --git a/homeassistant/components/samsungtv/translations/ca.json b/homeassistant/components/samsungtv/translations/ca.json
index 9ccff13ae3d..64e2298d141 100644
--- a/homeassistant/components/samsungtv/translations/ca.json
+++ b/homeassistant/components/samsungtv/translations/ca.json
@@ -3,21 +3,25 @@
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
"already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
- "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 del televisor per autoritzar a Home Assistant.",
+ "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 de dispositius externs del televisor per autoritzar Home Assistant.",
"cannot_connect": "Ha fallat la connexi\u00f3",
- "not_supported": "Actualment aquest televisor Samsung no \u00e9s compatible.",
+ "id_missing": "El dispositiu Samsung no t\u00e9 cap n\u00famero de s\u00e8rie.",
+ "not_supported": "Actualment aquest dispositiu Samsung no \u00e9s compatible.",
"reauth_successful": "Re-autenticaci\u00f3 realitzada correctament",
"unknown": "Error inesperat"
},
"error": {
- "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 del televisor per autoritzar a Home Assistant."
+ "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 de dispositius externs del televisor per autoritzar Home Assistant."
},
"flow_title": "{device}",
"step": {
"confirm": {
- "description": "Vols configurar el televisior Samsung {model}? Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent a la pantalla del televisor demanant autenticaci\u00f3. Les configuracions manuals d'aquest televisor es sobreescriuran.",
+ "description": "Vols configurar {device}? Si mai abans has connectat Home Assistant hauries de veure una finestra emergent a la pantalla del televisor demanant autenticaci\u00f3.",
"title": "Televisor Samsung"
},
+ "reauth_confirm": {
+ "description": "Despr\u00e9s d'enviar, tens 30 segons per acceptar la finestra emergent de {device} que sol\u00b7licita autoritzaci\u00f3."
+ },
"user": {
"data": {
"host": "Amfitri\u00f3",
diff --git a/homeassistant/components/samsungtv/translations/cs.json b/homeassistant/components/samsungtv/translations/cs.json
index f141dc3a09b..4453c7d227e 100644
--- a/homeassistant/components/samsungtv/translations/cs.json
+++ b/homeassistant/components/samsungtv/translations/cs.json
@@ -10,7 +10,7 @@
"flow_title": "Samsung TV: {model}",
"step": {
"confirm": {
- "description": "Chcete nastavit televizi Samsung {model} ? Pokud jste Home Assistant doposud nikdy nep\u0159ipojili, m\u011bla by se v\u00e1m na televizi zobrazit \u017e\u00e1dost o povolen\u00ed. Ru\u010dn\u00ed konfigurace pro tuto televizi budou p\u0159eps\u00e1ny.",
+ "description": "Chcete nastavit {device}? Pokud jste Home Assistant doposud nikdy nep\u0159ipojili, m\u011bla by se v\u00e1m na televizi zobrazit \u017e\u00e1dost o povolen\u00ed.",
"title": "Samsung TV"
},
"user": {
diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json
index 3ba569c87db..710443bc24f 100644
--- a/homeassistant/components/samsungtv/translations/de.json
+++ b/homeassistant/components/samsungtv/translations/de.json
@@ -3,22 +3,31 @@
"abort": {
"already_configured": "Dieser Samsung TV ist bereits konfiguriert",
"already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
- "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe die Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.",
+ "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe den Ger\u00e4teverbindungsmanager in den Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.",
"cannot_connect": "Verbindung fehlgeschlagen",
- "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt."
+ "id_missing": "Dieses Samsung-Ger\u00e4t hat keine Seriennummer.",
+ "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt.",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich",
+ "unknown": "Unerwarteter Fehler"
},
- "flow_title": "Samsung TV: {model}",
+ "error": {
+ "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe den Ger\u00e4teverbindungsmanager in den Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren."
+ },
+ "flow_title": "{device}",
"step": {
"confirm": {
- "description": "M\u00f6chtest du Samsung TV {model} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Autorisierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben.",
+ "description": "M\u00f6chtest du Samsung TV {device} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du eine Meldung auf deinem Fernseher sehen, die nach einer Autorisierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben.",
"title": "Samsung TV"
},
+ "reauth_confirm": {
+ "description": "Akzeptieren Sie nach dem Absenden die Meldung auf {device}, das eine Autorisierung innerhalb von 30 Sekunden anfordert."
+ },
"user": {
"data": {
"host": "Host",
"name": "Name"
},
- "description": "Gib deine Samsung TV-Informationen ein. Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt."
+ "description": "Gib deine Samsung TV-Informationen ein. Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du eine Meldung auf deinem Fernseher sehen, die nach einer Authentifizierung fragt."
}
}
}
diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json
index ceb37aaee1b..0228ca3101f 100644
--- a/homeassistant/components/samsungtv/translations/es.json
+++ b/homeassistant/components/samsungtv/translations/es.json
@@ -5,6 +5,7 @@
"already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso",
"auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este televisor Samsung. Revisa la configuraci\u00f3n de tu televisor para autorizar a Home Assistant.",
"cannot_connect": "No se pudo conectar",
+ "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.",
"not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible."
},
"flow_title": "Televisor Samsung: {model}",
@@ -13,6 +14,9 @@
"description": "\u00bfQuieres configurar la televisi\u00f3n Samsung {model}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n. Cualquier configuraci\u00f3n manual de esta TV se sobreescribir\u00e1.",
"title": "Samsung TV"
},
+ "reauth_confirm": {
+ "description": "Despu\u00e9s de enviarlo, acepte la ventana emergente en {device} solicitando autorizaci\u00f3n dentro de los 30 segundos."
+ },
"user": {
"data": {
"host": "Host",
diff --git a/homeassistant/components/samsungtv/translations/he.json b/homeassistant/components/samsungtv/translations/he.json
new file mode 100644
index 00000000000..9f62de51b8c
--- /dev/null
+++ b/homeassistant/components/samsungtv/translations/he.json
@@ -0,0 +1,27 @@
+{
+ "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",
+ "auth_missing": "Home Assistant \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e8\u05e9\u05d4 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05d6\u05d5 \u05e9\u05dc Samsung. \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05de\u05e0\u05d4\u05dc \u05d4\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05d7\u05d9\u05e6\u05d5\u05e0\u05d9\u05d9\u05dd \u05e9\u05dc \u05d4\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d0\u05e9\u05e8 \u05d0\u05ea Home Assistant.",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "auth_missing": "Home Assistant \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e8\u05e9\u05d4 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05d6\u05d5 \u05e9\u05dc Samsung. \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05de\u05e0\u05d4\u05dc \u05d4\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05d7\u05d9\u05e6\u05d5\u05e0\u05d9\u05d9\u05dd \u05e9\u05dc \u05d4\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d0\u05e9\u05e8 \u05d0\u05ea Home Assistant."
+ },
+ "flow_title": "{device}",
+ "step": {
+ "confirm": {
+ "title": "\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05e9\u05dc \u05e1\u05de\u05e1\u05d5\u05e0\u05d2"
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "name": "\u05e9\u05dd"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json
index 5c517c78d69..a720c5932ed 100644
--- a/homeassistant/components/samsungtv/translations/hu.json
+++ b/homeassistant/components/samsungtv/translations/hu.json
@@ -5,12 +5,17 @@
"already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
"auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizd a TV be\u00e1ll\u00edt\u00e1sait a Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.",
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
- "not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott."
+ "not_supported": "Ez a Samsung k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott.",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "Samsung TV: {model}",
+ "error": {
+ "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizd a TV be\u00e1ll\u00edt\u00e1sait a Home Assistant enged\u00e9lyez\u00e9s\u00e9hez."
+ },
+ "flow_title": "{device}",
"step": {
"confirm": {
- "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Samsung TV {model} k\u00e9sz\u00fcl\u00e9ket? Ha m\u00e9g soha nem csatlakozott Home Assistant-hez, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ahol hiteles\u00edt\u00e9st k\u00e9r. A TV manu\u00e1lis konfigur\u00e1ci\u00f3i fel\u00fcl\u00edr\u00f3dnak.",
+ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a(z) {device} k\u00e9sz\u00fcl\u00e9ket? Ha kor\u00e1bban m\u00e9g csatlakoztattad a Home Assistantet, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ami j\u00f3v\u00e1hagy\u00e1sra v\u00e1r.",
"title": "Samsung TV"
},
"user": {
diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json
index c64b8beca78..b4478994e1c 100644
--- a/homeassistant/components/samsungtv/translations/nl.json
+++ b/homeassistant/components/samsungtv/translations/nl.json
@@ -3,7 +3,7 @@
"abort": {
"already_configured": "Apparaat is al geconfigureerd",
"already_in_progress": "De configuratiestroom is al aan de gang",
- "auth_missing": "Home Assistant is niet geautoriseerd om verbinding te maken met deze Samsung TV.",
+ "auth_missing": "Home Assistant is niet gemachtigd om verbinding te maken met deze Samsung TV. Controleer de instellingen van Extern apparaatbeheer van uw tv om Home Assistant te machtigen.",
"cannot_connect": "Kan geen verbinding maken",
"id_missing": "Dit Samsung-apparaat heeft geen serienummer.",
"not_supported": "Deze Samsung TV wordt momenteel niet ondersteund.",
@@ -11,14 +11,17 @@
"unknown": "Onverwachte fout"
},
"error": {
- "auth_missing": "Home Assistant is niet geautoriseerd om verbinding te maken met deze Samsung TV."
+ "auth_missing": "Home Assistant is niet gemachtigd om verbinding te maken met deze Samsung TV. Controleer de instellingen van Extern apparaatbeheer van uw tv om Home Assistant te machtigen."
},
- "flow_title": "{model}",
+ "flow_title": "{device}",
"step": {
"confirm": {
- "description": "Wilt u Samsung TV {model} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven",
+ "description": "Wilt u Samsung TV {device} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven",
"title": "Samsung TV"
},
+ "reauth_confirm": {
+ "description": "Na het indienen, accepteer binnen 30 seconden de pop-up op {device} om autorisatie toe te staan."
+ },
"user": {
"data": {
"host": "Host",
diff --git a/homeassistant/components/samsungtv/translations/pl.json b/homeassistant/components/samsungtv/translations/pl.json
index 66d6ce3c4f3..ed117c60d21 100644
--- a/homeassistant/components/samsungtv/translations/pl.json
+++ b/homeassistant/components/samsungtv/translations/pl.json
@@ -3,16 +3,25 @@
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"already_in_progress": "Konfiguracja jest ju\u017c w toku",
- "auth_missing": "Home Assistant nie ma uprawnie\u0144 do po\u0142\u0105czenia si\u0119 z tym telewizorem Samsung. Sprawd\u017a ustawienia telewizora, aby autoryzowa\u0107 Home Assistant.",
+ "auth_missing": "Home Assistant nie ma uprawnie\u0144 do po\u0142\u0105czenia si\u0119 z tym telewizorem Samsung. Sprawd\u017a ustawienia \"Mened\u017cera urz\u0105dze\u0144 zewn\u0119trznych\", aby autoryzowa\u0107 Home Assistant.",
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
- "not_supported": "Ten telewizor Samsung nie jest obecnie obs\u0142ugiwany"
+ "id_missing": "To urz\u0105dzenie Samsung nie ma numeru seryjnego.",
+ "not_supported": "To urz\u0105dzenie Samsung nie jest obecnie obs\u0142ugiwane",
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
- "flow_title": "{model}",
+ "error": {
+ "auth_missing": "[%key::component::samsungtv::config::abort::auth_missing%]"
+ },
+ "flow_title": "{device}",
"step": {
"confirm": {
- "description": "Czy chcesz skonfigurowa\u0107 telewizor Samsung {model}? Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie. R\u0119czne konfiguracje tego telewizora zostan\u0105 zast\u0105pione.",
+ "description": "Czy chcesz skonfigurowa\u0107 {device}? Je\u015bli nigdy wcze\u015bniej nie \u0142\u0105czy\u0142e\u015b go z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie.",
"title": "Samsung TV"
},
+ "reauth_confirm": {
+ "description": "Po wys\u0142aniu \u017c\u0105dania, zaakceptuj wyskakuj\u0105ce okienko na {device} z pro\u015bb\u0105 o autoryzacj\u0119 w ci\u0105gu 30 sekund."
+ },
"user": {
"data": {
"host": "Nazwa hosta lub adres IP",
diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py
index ced56fe5905..16e09f2cf39 100644
--- a/homeassistant/components/scene/__init__.py
+++ b/homeassistant/components/scene/__init__.py
@@ -9,8 +9,9 @@ from typing import Any
import voluptuous as vol
from homeassistant.components.light import ATTR_TRANSITION
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON
-from homeassistant.core import DOMAIN as HA_DOMAIN
+from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
@@ -75,14 +76,16 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class Scene(Entity):
diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py
index 2225ef3d9dd..223ca9262ee 100644
--- a/homeassistant/components/screenlogic/__init__.py
+++ b/homeassistant/components/screenlogic/__init__.py
@@ -38,27 +38,10 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Screenlogic from a config entry."""
- mac = entry.unique_id
- # Attempt to re-discover named gateway to follow IP changes
- discovered_gateways = hass.data[DOMAIN][DISCOVERED_GATEWAYS]
- if mac in discovered_gateways:
- connect_info = discovered_gateways[mac]
- else:
- _LOGGER.warning("Gateway rediscovery failed")
- # Static connection defined or fallback from discovery
- connect_info = {
- SL_GATEWAY_NAME: name_for_mac(mac),
- SL_GATEWAY_IP: entry.data[CONF_IP_ADDRESS],
- SL_GATEWAY_PORT: entry.data[CONF_PORT],
- }
- try:
- gateway = ScreenLogicGateway(**connect_info)
- except ScreenLogicError as ex:
- _LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex)
- raise ConfigEntryNotReady from ex
+ gateway = await hass.async_add_executor_job(get_new_gateway, hass, entry)
# The api library uses a shared socket connection and does not handle concurrent
# requests very well.
@@ -99,6 +82,39 @@ async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
await hass.config_entries.async_reload(entry.entry_id)
+def get_connect_info(hass: HomeAssistant, entry: ConfigEntry):
+ """Construct connect_info from configuration entry and returns it to caller."""
+ mac = entry.unique_id
+ # Attempt to re-discover named gateway to follow IP changes
+ discovered_gateways = hass.data[DOMAIN][DISCOVERED_GATEWAYS]
+ if mac in discovered_gateways:
+ connect_info = discovered_gateways[mac]
+ else:
+ _LOGGER.warning("Gateway rediscovery failed")
+ # Static connection defined or fallback from discovery
+ connect_info = {
+ SL_GATEWAY_NAME: name_for_mac(mac),
+ SL_GATEWAY_IP: entry.data[CONF_IP_ADDRESS],
+ SL_GATEWAY_PORT: entry.data[CONF_PORT],
+ }
+
+ return connect_info
+
+
+def get_new_gateway(hass: HomeAssistant, entry: ConfigEntry):
+ """Instantiate a new ScreenLogicGateway, connect to it and return it to caller."""
+
+ connect_info = get_connect_info(hass, entry)
+
+ try:
+ gateway = ScreenLogicGateway(**connect_info)
+ except ScreenLogicError as ex:
+ _LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex)
+ raise ConfigEntryNotReady from ex
+
+ return gateway
+
+
class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage the data update for the Screenlogic component."""
@@ -119,13 +135,32 @@ class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator):
update_interval=interval,
)
+ def reconnect_gateway(self):
+ """Instantiate a new ScreenLogicGateway, connect to it and update. Return new gateway to caller."""
+
+ connect_info = get_connect_info(self.hass, self.config_entry)
+
+ try:
+ gateway = ScreenLogicGateway(**connect_info)
+ gateway.update()
+ except ScreenLogicError as error:
+ raise UpdateFailed(error) from error
+
+ return gateway
+
async def _async_update_data(self):
"""Fetch data from the Screenlogic gateway."""
try:
async with self.api_lock:
await self.hass.async_add_executor_job(self.gateway.update)
except ScreenLogicError as error:
- raise UpdateFailed(error) from error
+ _LOGGER.warning("ScreenLogicError - attempting reconnect: %s", error)
+
+ async with self.api_lock:
+ self.gateway = await self.hass.async_add_executor_job(
+ self.reconnect_gateway
+ )
+
return self.gateway.get_data()
diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py
index 2419ee46eed..1ad18298655 100644
--- a/homeassistant/components/screenlogic/sensor.py
+++ b/homeassistant/components/screenlogic/sensor.py
@@ -68,11 +68,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
# Pump sensors
for pump_num, pump_data in coordinator.data[SL_DATA.KEY_PUMPS].items():
if pump_data["data"] != 0 and "currentWatts" in pump_data:
- entities.extend(
- ScreenLogicPumpSensor(coordinator, pump_num, pump_key)
- for pump_key in pump_data
- if pump_key in SUPPORTED_PUMP_SENSORS
- )
+ for pump_key in pump_data:
+ # Considerations for Intelliflow VF
+ if pump_data["pumpType"] == 1 and pump_key == "currentRPM":
+ continue
+ # Considerations for Intelliflow VS
+ if pump_data["pumpType"] == 2 and pump_key == "currentGPM":
+ continue
+ if pump_key in SUPPORTED_PUMP_SENSORS:
+ entities.append(
+ ScreenLogicPumpSensor(coordinator, pump_num, pump_key)
+ )
# IntelliChem sensors
if equipment_flags & EQUIPMENT.FLAG_INTELLICHEM:
diff --git a/homeassistant/components/screenlogic/translations/he.json b/homeassistant/components/screenlogic/translations/he.json
new file mode 100644
index 00000000000..8e592e373e6
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/hu.json b/homeassistant/components/screenlogic/translations/hu.json
index 59e48fda273..f46ab499a29 100644
--- a/homeassistant/components/screenlogic/translations/hu.json
+++ b/homeassistant/components/screenlogic/translations/hu.json
@@ -6,7 +6,7 @@
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
- "flow_title": "ScreenLogic {name}",
+ "flow_title": "{name}",
"step": {
"gateway_entry": {
"data": {
diff --git a/homeassistant/components/script/translations/he.json b/homeassistant/components/script/translations/he.json
index f003c2e1210..7c2b808c58d 100644
--- a/homeassistant/components/script/translations/he.json
+++ b/homeassistant/components/script/translations/he.json
@@ -5,5 +5,5 @@
"on": "\u05d3\u05dc\u05d5\u05e7"
}
},
- "title": "\u05ea\u05b7\u05e1\u05e8\u05b4\u05d9\u05d8"
+ "title": "\u05e1\u05e7\u05e8\u05d9\u05e4\u05d8"
}
\ No newline at end of file
diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py
index db97410469f..93da95bc550 100644
--- a/homeassistant/components/search/__init__.py
+++ b/homeassistant/components/search/__init__.py
@@ -8,6 +8,7 @@ from homeassistant.components import automation, group, script, websocket_api
from homeassistant.components.homeassistant import scene
from homeassistant.core import HomeAssistant, callback, split_entity_id
from homeassistant.helpers import device_registry, entity_registry
+from homeassistant.helpers.entity import entity_sources as get_entity_sources
DOMAIN = "search"
_LOGGER = logging.getLogger(__name__)
@@ -44,6 +45,7 @@ def websocket_search_related(hass, connection, msg):
hass,
device_registry.async_get(hass),
entity_registry.async_get(hass),
+ get_entity_sources(hass),
)
connection.send_result(
msg["id"], searcher.async_search(msg["item_type"], msg["item_id"])
@@ -69,11 +71,13 @@ class Searcher:
hass: HomeAssistant,
device_reg: device_registry.DeviceRegistry,
entity_reg: entity_registry.EntityRegistry,
+ entity_sources: "dict[str, dict[str, str]]",
) -> None:
"""Search results."""
self.hass = hass
self._device_reg = device_reg
self._entity_reg = entity_reg
+ self._sources = entity_sources
self.results = defaultdict(set)
self._to_resolve = deque()
@@ -184,6 +188,10 @@ class Searcher:
if entity_entry.config_entry_id is not None:
self._add_or_resolve("config_entry", entity_entry.config_entry_id)
+ else:
+ source = self._sources.get(entity_id)
+ if source is not None and "config_entry" in source:
+ self._add_or_resolve("config_entry", source["config_entry"])
domain = split_entity_id(entity_id)[0]
diff --git a/homeassistant/components/season/translations/sensor.he.json b/homeassistant/components/season/translations/sensor.he.json
new file mode 100644
index 00000000000..a908ece48de
--- /dev/null
+++ b/homeassistant/components/season/translations/sensor.he.json
@@ -0,0 +1,16 @@
+{
+ "state": {
+ "season__season": {
+ "autumn": "\u05e1\u05ea\u05d9\u05d5",
+ "spring": "\u05d0\u05d1\u05d9\u05d1",
+ "summer": "\u05e7\u05d9\u05e5",
+ "winter": "\u05d7\u05d5\u05e8\u05e3"
+ },
+ "season__season__": {
+ "autumn": "\u05e1\u05ea\u05d9\u05d5",
+ "spring": "\u05d0\u05d1\u05d9\u05d1",
+ "summer": "\u05e7\u05d9\u05e5",
+ "winter": "\u05d7\u05d5\u05e8\u05e3"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py
new file mode 100644
index 00000000000..4ec8c46ef05
--- /dev/null
+++ b/homeassistant/components/select/__init__.py
@@ -0,0 +1,98 @@
+"""Component to allow selecting an option from a list as platforms."""
+from __future__ import annotations
+
+from datetime import timedelta
+import logging
+from typing import Any, final
+
+import voluptuous as vol
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.config_validation import ( # noqa: F401
+ PLATFORM_SCHEMA,
+ PLATFORM_SCHEMA_BASE,
+)
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.typing import ConfigType
+
+from .const import ATTR_OPTION, ATTR_OPTIONS, DOMAIN, SERVICE_SELECT_OPTION
+
+SCAN_INTERVAL = timedelta(seconds=30)
+
+ENTITY_ID_FORMAT = DOMAIN + ".{}"
+
+MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+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_SELECT_OPTION,
+ {vol.Required(ATTR_OPTION): cv.string},
+ "async_select_option",
+ )
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up a config entry."""
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
+
+
+class SelectEntity(Entity):
+ """Representation of a Select entity."""
+
+ _attr_current_option: str | None
+ _attr_options: list[str]
+ _attr_state: None = None
+
+ @property
+ def capability_attributes(self) -> dict[str, Any]:
+ """Return capability attributes."""
+ return {
+ ATTR_OPTIONS: self.options,
+ }
+
+ @property
+ @final
+ def state(self) -> str | None:
+ """Return the entity state."""
+ if self.current_option is None or self.current_option not in self.options:
+ return None
+ return self.current_option
+
+ @property
+ def options(self) -> list[str]:
+ """Return a set of selectable options."""
+ return self._attr_options
+
+ @property
+ def current_option(self) -> str | None:
+ """Return the selected entity option to represent the entity state."""
+ return self._attr_current_option
+
+ def select_option(self, option: str) -> None:
+ """Change the selected option."""
+ raise NotImplementedError()
+
+ async def async_select_option(self, option: str) -> None:
+ """Change the selected option."""
+ await self.hass.async_add_executor_job(self.select_option, option)
diff --git a/homeassistant/components/select/const.py b/homeassistant/components/select/const.py
new file mode 100644
index 00000000000..1f4615b0b05
--- /dev/null
+++ b/homeassistant/components/select/const.py
@@ -0,0 +1,10 @@
+"""Provides the constants needed for the component."""
+
+DOMAIN = "select"
+
+ATTR_OPTIONS = "options"
+ATTR_OPTION = "option"
+
+CONF_OPTION = "option"
+
+SERVICE_SELECT_OPTION = "select_option"
diff --git a/homeassistant/components/select/device_action.py b/homeassistant/components/select/device_action.py
new file mode 100644
index 00000000000..ece3c981690
--- /dev/null
+++ b/homeassistant/components/select/device_action.py
@@ -0,0 +1,74 @@
+"""Provides device actions for Select."""
+from __future__ import annotations
+
+from typing import Any
+
+import voluptuous as vol
+
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ CONF_ENTITY_ID,
+ CONF_TYPE,
+)
+from homeassistant.core import Context, HomeAssistant, HomeAssistantError
+from homeassistant.helpers import entity_registry
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import get_capability
+from homeassistant.helpers.typing import ConfigType
+
+from .const import ATTR_OPTION, ATTR_OPTIONS, CONF_OPTION, DOMAIN, SERVICE_SELECT_OPTION
+
+ACTION_TYPES = {"select_option"}
+
+ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_TYPE): vol.In(ACTION_TYPES),
+ vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
+ vol.Required(CONF_OPTION): str,
+ }
+)
+
+
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
+ """List device actions for Select devices."""
+ registry = await entity_registry.async_get_registry(hass)
+ return [
+ {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "select_option",
+ }
+ for entry in entity_registry.async_entries_for_device(registry, device_id)
+ if entry.domain == DOMAIN
+ ]
+
+
+async def async_call_action_from_config(
+ hass: HomeAssistant, config: dict, variables: dict, context: Context | None
+) -> None:
+ """Execute a device action."""
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: config[CONF_ENTITY_ID],
+ ATTR_OPTION: config[CONF_OPTION],
+ },
+ blocking=True,
+ context=context,
+ )
+
+
+async def async_get_action_capabilities(
+ hass: HomeAssistant, config: ConfigType
+) -> dict[str, Any]:
+ """List action capabilities."""
+ try:
+ options = get_capability(hass, config[CONF_ENTITY_ID], ATTR_OPTIONS) or []
+ except HomeAssistantError:
+ options = []
+
+ return {"extra_fields": vol.Schema({vol.Required(CONF_OPTION): vol.In(options)})}
diff --git a/homeassistant/components/select/device_condition.py b/homeassistant/components/select/device_condition.py
new file mode 100644
index 00000000000..ad82c432ce2
--- /dev/null
+++ b/homeassistant/components/select/device_condition.py
@@ -0,0 +1,88 @@
+"""Provide the device conditions for Select."""
+from __future__ import annotations
+
+from typing import Any
+
+import voluptuous as vol
+
+from homeassistant.const import (
+ CONF_CONDITION,
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ CONF_ENTITY_ID,
+ CONF_FOR,
+ CONF_TYPE,
+)
+from homeassistant.core import HomeAssistant, HomeAssistantError, callback
+from homeassistant.helpers import condition, config_validation as cv, entity_registry
+from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
+from homeassistant.helpers.entity import get_capability
+from homeassistant.helpers.typing import ConfigType, TemplateVarsType
+
+from .const import ATTR_OPTIONS, CONF_OPTION, DOMAIN
+
+CONDITION_TYPES = {"selected_option"}
+
+CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES),
+ vol.Required(CONF_OPTION): str,
+ vol.Optional(CONF_FOR): cv.positive_time_period_dict,
+ }
+)
+
+
+async def async_get_conditions(
+ hass: HomeAssistant, device_id: str
+) -> list[dict[str, str]]:
+ """List device conditions for Select devices."""
+ registry = await entity_registry.async_get_registry(hass)
+ return [
+ {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "selected_option",
+ }
+ for entry in entity_registry.async_entries_for_device(registry, device_id)
+ if entry.domain == DOMAIN
+ ]
+
+
+@callback
+def async_condition_from_config(
+ config: ConfigType, config_validation: bool
+) -> condition.ConditionCheckerType:
+ """Create a function to test a device condition."""
+ if config_validation:
+ config = CONDITION_SCHEMA(config)
+
+ @callback
+ def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
+ """Test if an entity is a certain state."""
+ return condition.state(
+ hass, config[CONF_ENTITY_ID], config[CONF_OPTION], config.get(CONF_FOR)
+ )
+
+ return test_is_state
+
+
+async def async_get_condition_capabilities(
+ hass: HomeAssistant, config: ConfigType
+) -> dict[str, Any]:
+ """List condition capabilities."""
+ try:
+ options = get_capability(hass, config[CONF_ENTITY_ID], ATTR_OPTIONS) or []
+ except HomeAssistantError:
+ options = []
+
+ return {
+ "extra_fields": vol.Schema(
+ {
+ vol.Required(CONF_OPTION): vol.In(options),
+ vol.Optional(CONF_FOR): cv.positive_time_period_dict,
+ }
+ )
+ }
diff --git a/homeassistant/components/select/device_trigger.py b/homeassistant/components/select/device_trigger.py
new file mode 100644
index 00000000000..84f61dfaec9
--- /dev/null
+++ b/homeassistant/components/select/device_trigger.py
@@ -0,0 +1,105 @@
+"""Provides device triggers for Select."""
+from __future__ import annotations
+
+from typing import Any
+
+import voluptuous as vol
+
+from homeassistant.components.automation import AutomationActionType
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
+from homeassistant.components.homeassistant.triggers.state import (
+ CONF_FOR,
+ CONF_FROM,
+ CONF_TO,
+ TRIGGER_SCHEMA as STATE_TRIGGER_SCHEMA,
+ async_attach_trigger as async_attach_state_trigger,
+)
+from homeassistant.components.select.const import ATTR_OPTIONS
+from homeassistant.const import (
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ CONF_ENTITY_ID,
+ CONF_PLATFORM,
+ CONF_TYPE,
+)
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant, HomeAssistantError
+from homeassistant.helpers import config_validation as cv, entity_registry
+from homeassistant.helpers.entity import get_capability
+from homeassistant.helpers.typing import ConfigType
+
+from . import DOMAIN
+
+TRIGGER_TYPES = {"current_option_changed"}
+
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
+ vol.Optional(CONF_TO): vol.Any(vol.Coerce(str)),
+ vol.Optional(CONF_FROM): vol.Any(vol.Coerce(str)),
+ vol.Optional(CONF_FOR): cv.positive_time_period_dict,
+ }
+)
+
+
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
+ """List device triggers for Select devices."""
+ registry = await entity_registry.async_get_registry(hass)
+ return [
+ {
+ CONF_PLATFORM: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "current_option_changed",
+ }
+ for entry in entity_registry.async_entries_for_device(registry, device_id)
+ if entry.domain == DOMAIN
+ ]
+
+
+async def async_attach_trigger(
+ hass: HomeAssistant,
+ config: ConfigType,
+ action: AutomationActionType,
+ automation_info: dict,
+) -> CALLBACK_TYPE:
+ """Attach a trigger."""
+ state_config = {
+ CONF_PLATFORM: "state",
+ CONF_ENTITY_ID: config[CONF_ENTITY_ID],
+ }
+
+ if CONF_TO in config:
+ state_config[CONF_TO] = config[CONF_TO]
+
+ if CONF_FROM in config:
+ state_config[CONF_FROM] = config[CONF_FROM]
+
+ if CONF_FOR in config:
+ state_config[CONF_FOR] = config[CONF_FOR]
+
+ state_config = STATE_TRIGGER_SCHEMA(state_config)
+ return await async_attach_state_trigger(
+ hass, state_config, action, automation_info, platform_type="device"
+ )
+
+
+async def async_get_trigger_capabilities(
+ hass: HomeAssistant, config: ConfigType
+) -> dict[str, Any]:
+ """List trigger capabilities."""
+ try:
+ options = get_capability(hass, config[CONF_ENTITY_ID], ATTR_OPTIONS) or []
+ except HomeAssistantError:
+ options = []
+
+ return {
+ "extra_fields": vol.Schema(
+ {
+ vol.Optional(CONF_FROM): vol.In(options),
+ vol.Optional(CONF_TO): vol.In(options),
+ vol.Optional(CONF_FOR): cv.positive_time_period_dict,
+ }
+ )
+ }
diff --git a/homeassistant/components/select/manifest.json b/homeassistant/components/select/manifest.json
new file mode 100644
index 00000000000..86e8b917199
--- /dev/null
+++ b/homeassistant/components/select/manifest.json
@@ -0,0 +1,7 @@
+{
+ "domain": "select",
+ "name": "Select",
+ "documentation": "https://www.home-assistant.io/integrations/select",
+ "codeowners": ["@home-assistant/core"],
+ "quality_scale": "internal"
+}
diff --git a/homeassistant/components/select/reproduce_state.py b/homeassistant/components/select/reproduce_state.py
new file mode 100644
index 00000000000..8af4b94fd6f
--- /dev/null
+++ b/homeassistant/components/select/reproduce_state.py
@@ -0,0 +1,66 @@
+"""Reproduce a Select entity state."""
+from __future__ import annotations
+
+import asyncio
+from collections.abc import Iterable
+import logging
+from typing import Any
+
+from homeassistant.components.select.const import ATTR_OPTIONS
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import Context, HomeAssistant, State
+
+from . import ATTR_OPTION, DOMAIN, SERVICE_SELECT_OPTION
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistant,
+ state: State,
+ *,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ if state.state not in cur_state.attributes.get(ATTR_OPTIONS, []):
+ _LOGGER.warning(
+ "Invalid state specified for %s: %s", state.entity_id, state.state
+ )
+ return
+
+ # Return if we are already at the right state.
+ if cur_state.state == state.state:
+ return
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {ATTR_ENTITY_ID: state.entity_id, ATTR_OPTION: state.state},
+ context=context,
+ blocking=True,
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistant,
+ states: Iterable[State],
+ *,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
+) -> None:
+ """Reproduce multiple select states."""
+ await asyncio.gather(
+ *(
+ _async_reproduce_state(
+ hass, state, context=context, reproduce_options=reproduce_options
+ )
+ for state in states
+ )
+ )
diff --git a/homeassistant/components/select/services.yaml b/homeassistant/components/select/services.yaml
new file mode 100644
index 00000000000..edf7fb50f00
--- /dev/null
+++ b/homeassistant/components/select/services.yaml
@@ -0,0 +1,14 @@
+select_option:
+ name: Select
+ description: Select an option of an select entity.
+ target:
+ entity:
+ domain: select
+ fields:
+ option:
+ name: Option
+ description: Option to be selected.
+ required: true
+ example: '"Item A"'
+ selector:
+ text:
diff --git a/homeassistant/components/select/significant_change.py b/homeassistant/components/select/significant_change.py
new file mode 100644
index 00000000000..835db314a38
--- /dev/null
+++ b/homeassistant/components/select/significant_change.py
@@ -0,0 +1,19 @@
+"""Helper to test significant Select state changes."""
+from __future__ import annotations
+
+from typing import Any
+
+from homeassistant.core import HomeAssistant, callback
+
+
+@callback
+def async_check_significant_change(
+ hass: HomeAssistant,
+ old_state: str,
+ old_attrs: dict,
+ new_state: str,
+ new_attrs: dict,
+ **kwargs: Any,
+) -> bool:
+ """Test if state significantly changed."""
+ return old_state != new_state
diff --git a/homeassistant/components/select/strings.json b/homeassistant/components/select/strings.json
new file mode 100644
index 00000000000..5724ff67a14
--- /dev/null
+++ b/homeassistant/components/select/strings.json
@@ -0,0 +1,14 @@
+{
+ "title": "Select",
+ "device_automation": {
+ "trigger_type": {
+ "current_option_changed": "{entity_name} option changed"
+ },
+ "action_type": {
+ "select_option": "Change {entity_name} option"
+ },
+ "condition_type": {
+ "selected_option": "Current {entity_name} selected option"
+ }
+ }
+}
diff --git a/homeassistant/components/select/translations/ca.json b/homeassistant/components/select/translations/ca.json
new file mode 100644
index 00000000000..bcd9b348cbb
--- /dev/null
+++ b/homeassistant/components/select/translations/ca.json
@@ -0,0 +1,14 @@
+{
+ "device_automation": {
+ "action_type": {
+ "select_option": "Canvia l'opci\u00f3 de {entity_name}"
+ },
+ "condition_type": {
+ "selected_option": "Opci\u00f3 actual seleccionada de {entity_name}"
+ },
+ "trigger_type": {
+ "current_option_changed": "{entity_name} canvi\u00ef d'opci\u00f3"
+ }
+ },
+ "title": "Selector"
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/translations/de.json b/homeassistant/components/select/translations/de.json
new file mode 100644
index 00000000000..a54098b11ac
--- /dev/null
+++ b/homeassistant/components/select/translations/de.json
@@ -0,0 +1,14 @@
+{
+ "device_automation": {
+ "action_type": {
+ "select_option": "Option {entity_name} \u00e4ndern"
+ },
+ "condition_type": {
+ "selected_option": "Aktuelle {entity_name} ausgew\u00e4hlte Option"
+ },
+ "trigger_type": {
+ "current_option_changed": "Option {entity_name} ge\u00e4ndert"
+ }
+ },
+ "title": "Ausw\u00e4hlen"
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/translations/en.json b/homeassistant/components/select/translations/en.json
new file mode 100644
index 00000000000..7f126ef36e8
--- /dev/null
+++ b/homeassistant/components/select/translations/en.json
@@ -0,0 +1,14 @@
+{
+ "device_automation": {
+ "action_type": {
+ "select_option": "Change {entity_name} option"
+ },
+ "condition_type": {
+ "selected_option": "Current {entity_name} selected option"
+ },
+ "trigger_type": {
+ "current_option_changed": "{entity_name} option changed"
+ }
+ },
+ "title": "Select"
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/translations/es.json b/homeassistant/components/select/translations/es.json
new file mode 100644
index 00000000000..193c6b702c7
--- /dev/null
+++ b/homeassistant/components/select/translations/es.json
@@ -0,0 +1,3 @@
+{
+ "title": "Seleccionar"
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/translations/et.json b/homeassistant/components/select/translations/et.json
new file mode 100644
index 00000000000..7fff5de9f5c
--- /dev/null
+++ b/homeassistant/components/select/translations/et.json
@@ -0,0 +1,14 @@
+{
+ "device_automation": {
+ "action_type": {
+ "select_option": "Muuda {entity_name} s\u00e4tteid"
+ },
+ "condition_type": {
+ "selected_option": "Praegused {entity_name} s\u00e4tted"
+ },
+ "trigger_type": {
+ "current_option_changed": "Olemi {entity_name} s\u00e4tted on muudetud"
+ }
+ },
+ "title": "Vali"
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/translations/hu.json b/homeassistant/components/select/translations/hu.json
new file mode 100644
index 00000000000..f93f0475c33
--- /dev/null
+++ b/homeassistant/components/select/translations/hu.json
@@ -0,0 +1,14 @@
+{
+ "device_automation": {
+ "action_type": {
+ "select_option": "M\u00f3dos\u00edtsa a(z) {entity_name} be\u00e1ll\u00edt\u00e1st"
+ },
+ "condition_type": {
+ "selected_option": "{entity_name} aktu\u00e1lisan kiv\u00e1lasztott opci\u00f3"
+ },
+ "trigger_type": {
+ "current_option_changed": "{entity_name} opci\u00f3i megv\u00e1ltoztak"
+ }
+ },
+ "title": "Kiv\u00e1laszt\u00e1s"
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/translations/it.json b/homeassistant/components/select/translations/it.json
new file mode 100644
index 00000000000..06a3f47ce7d
--- /dev/null
+++ b/homeassistant/components/select/translations/it.json
@@ -0,0 +1,14 @@
+{
+ "device_automation": {
+ "action_type": {
+ "select_option": "Cambia l'opzione {entity_name}"
+ },
+ "condition_type": {
+ "selected_option": "Opzione selezionata {entity_name} corrente"
+ },
+ "trigger_type": {
+ "current_option_changed": "Opzione {entity_name} modificata"
+ }
+ },
+ "title": "Selezionare"
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/translations/nl.json b/homeassistant/components/select/translations/nl.json
new file mode 100644
index 00000000000..76bf789bb2f
--- /dev/null
+++ b/homeassistant/components/select/translations/nl.json
@@ -0,0 +1,14 @@
+{
+ "device_automation": {
+ "action_type": {
+ "select_option": "Verander {entity_name} optie"
+ },
+ "condition_type": {
+ "selected_option": "Huidige {entity_name} geselecteerde optie"
+ },
+ "trigger_type": {
+ "current_option_changed": "{entity_name} optie veranderd"
+ }
+ },
+ "title": "Selecteer"
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/translations/no.json b/homeassistant/components/select/translations/no.json
new file mode 100644
index 00000000000..1458b8aa5b4
--- /dev/null
+++ b/homeassistant/components/select/translations/no.json
@@ -0,0 +1,14 @@
+{
+ "device_automation": {
+ "action_type": {
+ "select_option": "Endre alternativet {entity_name}"
+ },
+ "condition_type": {
+ "selected_option": "Gjeldende {entity_name} valgt alternativ"
+ },
+ "trigger_type": {
+ "current_option_changed": "Alternativet {entity_name} er endret"
+ }
+ },
+ "title": "Velg"
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/translations/pl.json b/homeassistant/components/select/translations/pl.json
new file mode 100644
index 00000000000..102cfa68534
--- /dev/null
+++ b/homeassistant/components/select/translations/pl.json
@@ -0,0 +1,14 @@
+{
+ "device_automation": {
+ "action_type": {
+ "select_option": "Zmie\u0144 opcj\u0119 {entity_name}"
+ },
+ "condition_type": {
+ "selected_option": "Aktualnie wybrana opcja dla {entity_name}"
+ },
+ "trigger_type": {
+ "current_option_changed": "Zmieniono opcj\u0119 {entity_name}"
+ }
+ },
+ "title": "Wybierz"
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/translations/ru.json b/homeassistant/components/select/translations/ru.json
new file mode 100644
index 00000000000..5bbdd279b43
--- /dev/null
+++ b/homeassistant/components/select/translations/ru.json
@@ -0,0 +1,14 @@
+{
+ "device_automation": {
+ "action_type": {
+ "select_option": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0432\u044b\u0431\u043e\u0440\u0430 {entity_name}"
+ },
+ "condition_type": {
+ "selected_option": "{entity_name} \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u043c"
+ },
+ "trigger_type": {
+ "current_option_changed": "{entity_name} \u043c\u0435\u043d\u044f\u0435\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0432\u044b\u0431\u043e\u0440\u0430"
+ }
+ },
+ "title": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c"
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/translations/zh-Hans.json b/homeassistant/components/select/translations/zh-Hans.json
new file mode 100644
index 00000000000..4857f798f3b
--- /dev/null
+++ b/homeassistant/components/select/translations/zh-Hans.json
@@ -0,0 +1,14 @@
+{
+ "device_automation": {
+ "action_type": {
+ "select_option": "\u6539\u53d8 {entity_name} \u7684\u9009\u9879"
+ },
+ "condition_type": {
+ "selected_option": "{entity_name} \u5f53\u524d\u9009\u62e9\u9879"
+ },
+ "trigger_type": {
+ "current_option_changed": "{entity_name} \u7684\u9009\u9879\u53d8\u5316"
+ }
+ },
+ "title": "\u9009\u5b9a"
+}
\ No newline at end of file
diff --git a/homeassistant/components/select/translations/zh-Hant.json b/homeassistant/components/select/translations/zh-Hant.json
new file mode 100644
index 00000000000..2bf3114508a
--- /dev/null
+++ b/homeassistant/components/select/translations/zh-Hant.json
@@ -0,0 +1,14 @@
+{
+ "device_automation": {
+ "action_type": {
+ "select_option": "\u8b8a\u66f4{entity_name}\u9078\u9805"
+ },
+ "condition_type": {
+ "selected_option": "\u76ee\u524d{entity_name}\u5df2\u9078\u9078\u9805"
+ },
+ "trigger_type": {
+ "current_option_changed": "{entity_name} \u9078\u9805\u5df2\u8b8a\u66f4"
+ }
+ },
+ "title": "\u9078\u64c7"
+}
\ No newline at end of file
diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py
index e431fe2487b..bfc3f42b421 100644
--- a/homeassistant/components/sense/__init__.py
+++ b/homeassistant/components/sense/__init__.py
@@ -55,7 +55,7 @@ class SenseDevicesData:
return self._data_by_device.get(sense_device_id)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Sense from a config entry."""
entry_data = entry.data
diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py
index 0af64f3f17d..d779870d37d 100644
--- a/homeassistant/components/sense/sensor.py
+++ b/homeassistant/components/sense/sensor.py
@@ -1,5 +1,5 @@
"""Support for monitoring a Sense energy sensor."""
-from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
DEVICE_CLASS_POWER,
@@ -121,6 +121,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class SenseActiveSensor(SensorEntity):
"""Implementation of a Sense energy sensor."""
+ _attr_icon = ICON
+ _attr_unit_of_measurement = POWER_WATT
+ _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
+ _attr_should_poll = False
+ _attr_available = False
+ _attr_state_class = STATE_CLASS_MEASUREMENT
+
def __init__(
self,
data,
@@ -133,54 +140,12 @@ class SenseActiveSensor(SensorEntity):
):
"""Initialize the Sense sensor."""
name_type = PRODUCTION_NAME if is_production else CONSUMPTION_NAME
- self._name = f"{name} {name_type}"
- self._unique_id = unique_id
- self._available = False
+ self._attr_name = f"{name} {name_type}"
+ self._attr_unique_id = unique_id
self._data = data
self._sense_monitor_id = sense_monitor_id
self._sensor_type = sensor_type
self._is_production = is_production
- self._state = None
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def available(self):
- """Return the availability of the sensor."""
- return self._available
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return POWER_WATT
-
- @property
- def extra_state_attributes(self):
- """Return the state attributes."""
- return {ATTR_ATTRIBUTION: ATTRIBUTION}
-
- @property
- def icon(self):
- """Icon to use in the frontend, if any."""
- return ICON
-
- @property
- def unique_id(self):
- """Return the unique id."""
- return self._unique_id
-
- @property
- def should_poll(self):
- """Return the device should not poll for updates."""
- return False
async def async_added_to_hass(self):
"""Register callbacks."""
@@ -200,16 +165,22 @@ class SenseActiveSensor(SensorEntity):
if self._is_production
else self._data.active_power
)
- if self._available and self._state == new_state:
+ if self._attr_available and self._attr_state == new_state:
return
- self._state = new_state
- self._available = True
+ self._attr_state = new_state
+ self._attr_available = True
self.async_write_ha_state()
class SenseVoltageSensor(SensorEntity):
"""Implementation of a Sense energy voltage sensor."""
+ _attr_unit_of_measurement = VOLT
+ _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
+ _attr_icon = ICON
+ _attr_should_poll = False
+ _attr_available = False
+
def __init__(
self,
data,
@@ -218,53 +189,11 @@ class SenseVoltageSensor(SensorEntity):
):
"""Initialize the Sense sensor."""
line_num = index + 1
- self._name = f"L{line_num} Voltage"
- self._unique_id = f"{sense_monitor_id}-L{line_num}"
- self._available = False
+ self._attr_name = f"L{line_num} Voltage"
+ self._attr_unique_id = f"{sense_monitor_id}-L{line_num}"
self._data = data
self._sense_monitor_id = sense_monitor_id
self._voltage_index = index
- self._state = None
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def available(self):
- """Return the availability of the sensor."""
- return self._available
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return VOLT
-
- @property
- def extra_state_attributes(self):
- """Return the state attributes."""
- return {ATTR_ATTRIBUTION: ATTRIBUTION}
-
- @property
- def icon(self):
- """Icon to use in the frontend, if any."""
- return ICON
-
- @property
- def unique_id(self):
- """Return the unique id."""
- return self._unique_id
-
- @property
- def should_poll(self):
- """Return the device should not poll for updates."""
- return False
async def async_added_to_hass(self):
"""Register callbacks."""
@@ -280,16 +209,21 @@ class SenseVoltageSensor(SensorEntity):
def _async_update_from_data(self):
"""Update the sensor from the data. Must not do I/O."""
new_state = round(self._data.active_voltage[self._voltage_index], 1)
- if self._available and self._state == new_state:
+ if self._attr_available and self._attr_state == new_state:
return
- self._available = True
- self._state = new_state
+ self._attr_available = True
+ self._attr_state = new_state
self.async_write_ha_state()
class SenseTrendsSensor(SensorEntity):
"""Implementation of a Sense energy sensor."""
+ _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR
+ _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
+ _attr_icon = ICON
+ _attr_should_poll = False
+
def __init__(
self,
data,
@@ -301,22 +235,14 @@ class SenseTrendsSensor(SensorEntity):
):
"""Initialize the Sense sensor."""
name_type = PRODUCTION_NAME if is_production else CONSUMPTION_NAME
- self._name = f"{name} {name_type}"
- self._unique_id = unique_id
- self._available = False
+ self._attr_name = f"{name} {name_type}"
+ self._attr_unique_id = unique_id
self._data = data
self._sensor_type = sensor_type
self._coordinator = trends_coordinator
self._is_production = is_production
- self._state = None
- self._unit_of_measurement = ENERGY_KILO_WATT_HOUR
self._had_any_update = False
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
@property
def state(self):
"""Return the state of the sensor."""
@@ -327,31 +253,6 @@ class SenseTrendsSensor(SensorEntity):
"""Return if entity is available."""
return self._had_any_update and self._coordinator.last_update_success
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return self._unit_of_measurement
-
- @property
- def extra_state_attributes(self):
- """Return the state attributes."""
- return {ATTR_ATTRIBUTION: ATTRIBUTION}
-
- @property
- def icon(self):
- """Icon to use in the frontend, if any."""
- return ICON
-
- @property
- def unique_id(self):
- """Return the unique id."""
- return self._unique_id
-
- @property
- def should_poll(self):
- """No need to poll. Coordinator notifies entity of updates."""
- return False
-
@callback
def _async_update(self):
"""Track if we had an update so we do not report zero data."""
@@ -373,61 +274,21 @@ class SenseTrendsSensor(SensorEntity):
class SenseEnergyDevice(SensorEntity):
"""Implementation of a Sense energy device."""
+ _attr_available = False
+ _attr_state_class = STATE_CLASS_MEASUREMENT
+ _attr_unit_of_measurement = POWER_WATT
+ _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
+ _attr_device_class = DEVICE_CLASS_POWER
+ _attr_should_poll = False
+
def __init__(self, sense_devices_data, device, sense_monitor_id):
"""Initialize the Sense binary sensor."""
- self._name = f"{device['name']} {CONSUMPTION_NAME}"
+ self._attr_name = f"{device['name']} {CONSUMPTION_NAME}"
self._id = device["id"]
- self._available = False
self._sense_monitor_id = sense_monitor_id
- self._unique_id = f"{sense_monitor_id}-{self._id}-{CONSUMPTION_ID}"
- self._icon = sense_to_mdi(device["icon"])
+ self._attr_unique_id = f"{sense_monitor_id}-{self._id}-{CONSUMPTION_ID}"
+ self._attr_icon = sense_to_mdi(device["icon"])
self._sense_devices_data = sense_devices_data
- self._state = None
-
- @property
- def state(self):
- """Return the wattage of the sensor."""
- return self._state
-
- @property
- def available(self):
- """Return the availability of the sensor."""
- return self._available
-
- @property
- def name(self):
- """Return the name of the power sensor."""
- return self._name
-
- @property
- def unique_id(self):
- """Return the unique id of the power sensor."""
- return self._unique_id
-
- @property
- def icon(self):
- """Return the icon of the power sensor."""
- return self._icon
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity."""
- return POWER_WATT
-
- @property
- def extra_state_attributes(self):
- """Return the state attributes."""
- return {ATTR_ATTRIBUTION: ATTRIBUTION}
-
- @property
- def device_class(self):
- """Return the device class of the power sensor."""
- return DEVICE_CLASS_POWER
-
- @property
- def should_poll(self):
- """Return the device should not poll for updates."""
- return False
async def async_added_to_hass(self):
"""Register callbacks."""
@@ -447,8 +308,8 @@ class SenseEnergyDevice(SensorEntity):
new_state = 0
else:
new_state = int(device_data["w"])
- if self._available and self._state == new_state:
+ if self._attr_available and self._attr_state == new_state:
return
- self._state = new_state
- self._available = True
+ self._attr_state = new_state
+ self._attr_available = True
self.async_write_ha_state()
diff --git a/homeassistant/components/sense/translations/he.json b/homeassistant/components/sense/translations/he.json
index 3007c0e968c..ecb8a74bc6f 100644
--- a/homeassistant/components/sense/translations/he.json
+++ b/homeassistant/components/sense/translations/he.json
@@ -1,8 +1,17 @@
{
"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",
+ "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"
}
}
diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py
index 10ceaa39a38..d34ea040cdc 100644
--- a/homeassistant/components/sensibo/climate.py
+++ b/homeassistant/components/sensibo/climate.py
@@ -40,7 +40,7 @@ from .const import DOMAIN as SENSIBO_DOMAIN
_LOGGER = logging.getLogger(__name__)
ALL = ["all"]
-TIMEOUT = 10
+TIMEOUT = 8
SERVICE_ASSUME_STATE = "assume_state"
@@ -91,17 +91,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
devices = []
try:
- for dev in await client.async_get_devices(_INITIAL_FETCH_FIELDS):
- if config[CONF_ID] == ALL or dev["id"] in config[CONF_ID]:
- devices.append(
- SensiboClimate(client, dev, hass.config.units.temperature_unit)
- )
+ with async_timeout.timeout(TIMEOUT):
+ for dev in await client.async_get_devices(_INITIAL_FETCH_FIELDS):
+ if config[CONF_ID] == ALL or dev["id"] in config[CONF_ID]:
+ devices.append(
+ SensiboClimate(client, dev, hass.config.units.temperature_unit)
+ )
except (
aiohttp.client_exceptions.ClientConnectorError,
asyncio.TimeoutError,
pysensibo.SensiboError,
) as err:
- _LOGGER.exception("Failed to connect to Sensibo servers")
+ _LOGGER.error("Failed to get devices from Sensibo servers")
raise PlatformNotReady from err
if not devices:
@@ -150,6 +151,7 @@ class SensiboClimate(ClimateEntity):
self._units = units
self._available = False
self._do_update(data)
+ self._failed_update = False
@property
def supported_features(self):
@@ -316,59 +318,35 @@ class SensiboClimate(ClimateEntity):
else:
return
- with async_timeout.timeout(TIMEOUT):
- await self._client.async_set_ac_state_property(
- self._id, "targetTemperature", temperature, self._ac_states
- )
+ await self._async_set_ac_state_property("targetTemperature", temperature)
async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
- with async_timeout.timeout(TIMEOUT):
- await self._client.async_set_ac_state_property(
- self._id, "fanLevel", fan_mode, self._ac_states
- )
+ await self._async_set_ac_state_property("fanLevel", fan_mode)
async def async_set_hvac_mode(self, hvac_mode):
"""Set new target operation mode."""
if hvac_mode == HVAC_MODE_OFF:
- with async_timeout.timeout(TIMEOUT):
- await self._client.async_set_ac_state_property(
- self._id, "on", False, self._ac_states
- )
+ await self._async_set_ac_state_property("on", False)
return
# Turn on if not currently on.
if not self._ac_states["on"]:
- with async_timeout.timeout(TIMEOUT):
- await self._client.async_set_ac_state_property(
- self._id, "on", True, self._ac_states
- )
+ await self._async_set_ac_state_property("on", True)
- with async_timeout.timeout(TIMEOUT):
- await self._client.async_set_ac_state_property(
- self._id, "mode", HA_TO_SENSIBO[hvac_mode], self._ac_states
- )
+ await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode])
async def async_set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
- with async_timeout.timeout(TIMEOUT):
- await self._client.async_set_ac_state_property(
- self._id, "swing", swing_mode, self._ac_states
- )
+ await self._async_set_ac_state_property("swing", swing_mode)
async def async_turn_on(self):
"""Turn Sensibo unit on."""
- with async_timeout.timeout(TIMEOUT):
- await self._client.async_set_ac_state_property(
- self._id, "on", True, self._ac_states
- )
+ await self._async_set_ac_state_property("on", True)
async def async_turn_off(self):
"""Turn Sensibo unit on."""
- with async_timeout.timeout(TIMEOUT):
- await self._client.async_set_ac_state_property(
- self._id, "on", False, self._ac_states
- )
+ await self._async_set_ac_state_property("on", False)
async def async_assume_state(self, state):
"""Set external state."""
@@ -377,14 +355,7 @@ class SensiboClimate(ClimateEntity):
)
if change_needed:
- with async_timeout.timeout(TIMEOUT):
- await self._client.async_set_ac_state_property(
- self._id,
- "on",
- state != HVAC_MODE_OFF, # value
- self._ac_states,
- True, # assumed_state
- )
+ await self._async_set_ac_state_property("on", state != HVAC_MODE_OFF, True)
if state in [STATE_ON, HVAC_MODE_OFF]:
self._external_state = None
@@ -396,7 +367,41 @@ class SensiboClimate(ClimateEntity):
try:
with async_timeout.timeout(TIMEOUT):
data = await self._client.async_get_device(self._id, _FETCH_FIELDS)
- self._do_update(data)
- except (aiohttp.client_exceptions.ClientError, pysensibo.SensiboError):
- _LOGGER.warning("Failed to connect to Sensibo servers")
+ except (
+ aiohttp.client_exceptions.ClientError,
+ asyncio.TimeoutError,
+ pysensibo.SensiboError,
+ ):
+ if self._failed_update:
+ _LOGGER.warning(
+ "Failed to update data for device '%s' from Sensibo servers",
+ self.name,
+ )
+ self._available = False
+ self.async_write_ha_state()
+ return
+
+ _LOGGER.debug("First failed update data for device '%s'", self.name)
+ self._failed_update = True
+ return
+
+ self._failed_update = False
+ self._do_update(data)
+
+ async def _async_set_ac_state_property(self, name, value, assumed_state=False):
+ """Set AC state."""
+ try:
+ with async_timeout.timeout(TIMEOUT):
+ await self._client.async_set_ac_state_property(
+ self._id, name, value, self._ac_states, assumed_state
+ )
+ except (
+ aiohttp.client_exceptions.ClientError,
+ asyncio.TimeoutError,
+ pysensibo.SensiboError,
+ ) as err:
self._available = False
+ self.async_write_ha_state()
+ raise Exception(
+ f"Failed to set AC state for device {self.name} to Sensibo servers"
+ ) from err
diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index 2857a3cc5c2..8fac4e50b3e 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -17,6 +17,7 @@ from homeassistant.const import (
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
+ DEVICE_CLASS_MONETARY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_PRESSURE,
@@ -52,6 +53,7 @@ DEVICE_CLASSES: Final[list[str]] = [
DEVICE_CLASS_ENERGY, # energy (kWh, Wh)
DEVICE_CLASS_HUMIDITY, # % of humidity in the air
DEVICE_CLASS_ILLUMINANCE, # current light level (lx/lm)
+ DEVICE_CLASS_MONETARY, # Amount of money (currency)
DEVICE_CLASS_SIGNAL_STRENGTH, # signal strength (dB/dBm)
DEVICE_CLASS_TEMPERATURE, # temperature (C/F)
DEVICE_CLASS_TIMESTAMP, # timestamp (ISO8601)
diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py
index 4d3d8a4b477..a77ed2d2cd7 100644
--- a/homeassistant/components/sensor/device_condition.py
+++ b/homeassistant/components/sensor/device_condition.py
@@ -7,8 +7,6 @@ from homeassistant.components.device_automation.exceptions import (
InvalidDeviceAutomationConfig,
)
from homeassistant.const import (
- ATTR_DEVICE_CLASS,
- ATTR_UNIT_OF_MEASUREMENT,
CONF_ABOVE,
CONF_BELOW,
CONF_ENTITY_ID,
@@ -27,8 +25,9 @@ from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLTAGE,
)
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant, HomeAssistantError, callback
from homeassistant.helpers import condition, config_validation as cv
+from homeassistant.helpers.entity import get_device_class, get_unit_of_measurement
from homeassistant.helpers.entity_registry import (
async_entries_for_device,
async_get_registry,
@@ -116,18 +115,12 @@ async def async_get_conditions(
]
for entry in entries:
- device_class = DEVICE_CLASS_NONE
- state = hass.states.get(entry.entity_id)
- unit_of_measurement = (
- state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None
- )
+ device_class = get_device_class(hass, entry.entity_id) or DEVICE_CLASS_NONE
+ unit_of_measurement = get_unit_of_measurement(hass, entry.entity_id)
- if not state or not unit_of_measurement:
+ if not unit_of_measurement:
continue
- if ATTR_DEVICE_CLASS in state.attributes:
- device_class = state.attributes[ATTR_DEVICE_CLASS]
-
templates = ENTITY_CONDITIONS.get(
device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE]
)
@@ -167,15 +160,14 @@ def async_condition_from_config(
async def async_get_condition_capabilities(hass, config):
"""List condition capabilities."""
- state = hass.states.get(config[CONF_ENTITY_ID])
- unit_of_measurement = (
- state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None
- )
+ try:
+ unit_of_measurement = get_unit_of_measurement(hass, config[CONF_ENTITY_ID])
+ except HomeAssistantError:
+ unit_of_measurement = None
- if not state or not unit_of_measurement:
+ if not unit_of_measurement:
raise InvalidDeviceAutomationConfig(
- "No state or unit of measurement found for "
- f"condition entity {config[CONF_ENTITY_ID]}"
+ "No unit of measurement found for condition entity {config[CONF_ENTITY_ID]}"
)
return {
diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py
index 0bca1e299d6..3b00bae816d 100644
--- a/homeassistant/components/sensor/device_trigger.py
+++ b/homeassistant/components/sensor/device_trigger.py
@@ -1,7 +1,7 @@
"""Provides device triggers for sensors."""
import voluptuous as vol
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.device_automation.exceptions import (
InvalidDeviceAutomationConfig,
)
@@ -9,8 +9,6 @@ from homeassistant.components.homeassistant.triggers import (
numeric_state as numeric_state_trigger,
)
from homeassistant.const import (
- ATTR_DEVICE_CLASS,
- ATTR_UNIT_OF_MEASUREMENT,
CONF_ABOVE,
CONF_BELOW,
CONF_ENTITY_ID,
@@ -30,7 +28,9 @@ from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLTAGE,
)
+from homeassistant.core import HomeAssistantError
from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.entity import get_device_class, get_unit_of_measurement
from homeassistant.helpers.entity_registry import async_entries_for_device
from . import DOMAIN
@@ -73,7 +73,7 @@ ENTITY_TRIGGERS = {
TRIGGER_SCHEMA = vol.All(
- TRIGGER_BASE_SCHEMA.extend(
+ DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(
@@ -134,18 +134,12 @@ async def async_get_triggers(hass, device_id):
]
for entry in entries:
- device_class = DEVICE_CLASS_NONE
- state = hass.states.get(entry.entity_id)
- unit_of_measurement = (
- state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None
- )
+ device_class = get_device_class(hass, entry.entity_id) or DEVICE_CLASS_NONE
+ unit_of_measurement = get_unit_of_measurement(hass, entry.entity_id)
- if not state or not unit_of_measurement:
+ if not unit_of_measurement:
continue
- if ATTR_DEVICE_CLASS in state.attributes:
- device_class = state.attributes[ATTR_DEVICE_CLASS]
-
templates = ENTITY_TRIGGERS.get(
device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE]
)
@@ -166,15 +160,14 @@ async def async_get_triggers(hass, device_id):
async def async_get_trigger_capabilities(hass, config):
"""List trigger capabilities."""
- state = hass.states.get(config[CONF_ENTITY_ID])
- unit_of_measurement = (
- state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None
- )
+ try:
+ unit_of_measurement = get_unit_of_measurement(hass, config[CONF_ENTITY_ID])
+ except HomeAssistantError:
+ unit_of_measurement = None
- if not state or not unit_of_measurement:
+ if not unit_of_measurement:
raise InvalidDeviceAutomationConfig(
- "No state or unit of measurement found for "
- f"trigger entity {config[CONF_ENTITY_ID]}"
+ f"No unit of measurement found for trigger entity {config[CONF_ENTITY_ID]}"
)
return {
diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py
index fb6c8d2fba3..bbd49814076 100644
--- a/homeassistant/components/sensor/recorder.py
+++ b/homeassistant/components/sensor/recorder.py
@@ -3,6 +3,8 @@ from __future__ import annotations
import datetime
import itertools
+import logging
+from typing import Callable
from homeassistant.components.recorder import history, statistics
from homeassistant.components.sensor import (
@@ -10,24 +12,89 @@ from homeassistant.components.sensor import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_MONETARY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
)
-from homeassistant.const import ATTR_DEVICE_CLASS
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_UNIT_OF_MEASUREMENT,
+ DEVICE_CLASS_POWER,
+ ENERGY_KILO_WATT_HOUR,
+ ENERGY_WATT_HOUR,
+ POWER_KILO_WATT,
+ POWER_WATT,
+ PRESSURE_BAR,
+ PRESSURE_HPA,
+ PRESSURE_INHG,
+ PRESSURE_MBAR,
+ PRESSURE_PA,
+ PRESSURE_PSI,
+ TEMP_CELSIUS,
+ TEMP_FAHRENHEIT,
+ TEMP_KELVIN,
+)
from homeassistant.core import HomeAssistant, State
import homeassistant.util.dt as dt_util
+import homeassistant.util.pressure as pressure_util
+import homeassistant.util.temperature as temperature_util
from . import DOMAIN
+_LOGGER = logging.getLogger(__name__)
+
DEVICE_CLASS_STATISTICS = {
DEVICE_CLASS_BATTERY: {"mean", "min", "max"},
DEVICE_CLASS_ENERGY: {"sum"},
DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"},
+ DEVICE_CLASS_MONETARY: {"sum"},
+ DEVICE_CLASS_POWER: {"mean", "min", "max"},
DEVICE_CLASS_PRESSURE: {"mean", "min", "max"},
DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"},
}
+# Normalized units which will be stored in the statistics table
+DEVICE_CLASS_UNITS = {
+ DEVICE_CLASS_ENERGY: ENERGY_KILO_WATT_HOUR,
+ DEVICE_CLASS_POWER: POWER_WATT,
+ DEVICE_CLASS_PRESSURE: PRESSURE_PA,
+ DEVICE_CLASS_TEMPERATURE: TEMP_CELSIUS,
+}
+
+UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = {
+ # Convert energy to kWh
+ DEVICE_CLASS_ENERGY: {
+ ENERGY_KILO_WATT_HOUR: lambda x: x,
+ ENERGY_WATT_HOUR: lambda x: x / 1000,
+ },
+ # Convert power W
+ DEVICE_CLASS_POWER: {
+ POWER_WATT: lambda x: x,
+ POWER_KILO_WATT: lambda x: x * 1000,
+ },
+ # Convert pressure to Pa
+ # Note: pressure_util.convert is bypassed to avoid redundant error checking
+ DEVICE_CLASS_PRESSURE: {
+ PRESSURE_BAR: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_BAR],
+ PRESSURE_HPA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_HPA],
+ PRESSURE_INHG: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_INHG],
+ PRESSURE_MBAR: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_MBAR],
+ PRESSURE_PA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PA],
+ PRESSURE_PSI: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PSI],
+ },
+ # Convert temperature to °C
+ # Note: temperature_util.convert is bypassed to avoid redundant error checking
+ DEVICE_CLASS_TEMPERATURE: {
+ TEMP_CELSIUS: lambda x: x,
+ TEMP_FAHRENHEIT: temperature_util.fahrenheit_to_celsius,
+ TEMP_KELVIN: temperature_util.kelvin_to_celsius,
+ },
+}
+
+# Keep track of entities for which a warning about unsupported unit has been logged
+WARN_UNSUPPORTED_UNIT = set()
+
def _get_entities(hass: HomeAssistant) -> list[tuple[str, str]]:
"""Get (entity_id, device_class) of all sensors for which to compile statistics."""
@@ -90,6 +157,42 @@ def _time_weighted_average(
return accumulated / (end - start).total_seconds()
+def _normalize_states(
+ entity_history: list[State], device_class: str, entity_id: str
+) -> tuple[str | None, list[tuple[float, State]]]:
+ """Normalize units."""
+ unit = None
+
+ if device_class not in UNIT_CONVERSIONS:
+ # We're not normalizing this device class, return the state as they are
+ fstates = [
+ (float(el.state), el) for el in entity_history if _is_number(el.state)
+ ]
+ if fstates:
+ unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ return unit, fstates
+
+ fstates = []
+
+ for state in entity_history:
+ # Exclude non numerical states from statistics
+ if not _is_number(state.state):
+ continue
+
+ fstate = float(state.state)
+ unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ # Exclude unsupported units from statistics
+ if unit not in UNIT_CONVERSIONS[device_class]:
+ if entity_id not in WARN_UNSUPPORTED_UNIT:
+ WARN_UNSUPPORTED_UNIT.add(entity_id)
+ _LOGGER.warning("%s has unknown unit %s", entity_id, unit)
+ continue
+
+ fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state))
+
+ return DEVICE_CLASS_UNITS[device_class], fstates
+
+
def compile_statistics(
hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime
) -> dict:
@@ -113,23 +216,29 @@ def compile_statistics(
continue
entity_history = history_list[entity_id]
- fstates = [
- (float(el.state), el) for el in entity_history if _is_number(el.state)
- ]
+ unit, fstates = _normalize_states(entity_history, device_class, entity_id)
if not fstates:
continue
result[entity_id] = {}
+ # Set meta data
+ result[entity_id]["meta"] = {
+ "unit_of_measurement": unit,
+ "has_mean": "mean" in wanted_statistics,
+ "has_sum": "sum" in wanted_statistics,
+ }
+
# Make calculations
+ stat: dict = {}
if "max" in wanted_statistics:
- result[entity_id]["max"] = max(*itertools.islice(zip(*fstates), 1))
+ stat["max"] = max(*itertools.islice(zip(*fstates), 1))
if "min" in wanted_statistics:
- result[entity_id]["min"] = min(*itertools.islice(zip(*fstates), 1))
+ stat["min"] = min(*itertools.islice(zip(*fstates), 1))
if "mean" in wanted_statistics:
- result[entity_id]["mean"] = _time_weighted_average(fstates, start, end)
+ stat["mean"] = _time_weighted_average(fstates, start, end)
if "sum" in wanted_statistics:
last_reset = old_last_reset = None
@@ -143,6 +252,7 @@ def compile_statistics(
_sum = last_stats[entity_id][0]["sum"]
for fstate, state in fstates:
+
if "last_reset" not in state.attributes:
continue
if (last_reset := state.attributes["last_reset"]) != old_last_reset:
@@ -163,8 +273,10 @@ def compile_statistics(
# Update the sum with the last state
_sum += new_state - old_state
- result[entity_id]["last_reset"] = dt_util.parse_datetime(last_reset)
- result[entity_id]["sum"] = _sum
- result[entity_id]["state"] = new_state
+ stat["last_reset"] = dt_util.parse_datetime(last_reset)
+ stat["sum"] = _sum
+ stat["state"] = new_state
+
+ result[entity_id]["stat"] = stat
return result
diff --git a/homeassistant/components/sentry/translations/de.json b/homeassistant/components/sentry/translations/de.json
index 8fbcfc1eaa2..62aca6a65e9 100644
--- a/homeassistant/components/sentry/translations/de.json
+++ b/homeassistant/components/sentry/translations/de.json
@@ -16,5 +16,21 @@
"title": "Sentry"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "environment": "Optionaler Name der Umgebung.",
+ "event_custom_components": "Senden von Ereignissen aus benutzerdefinierten Komponenten",
+ "event_handled": "Behandelte Ereignisse senden",
+ "event_third_party_packages": "Senden von Ereignissen aus Drittanbieterpaketen",
+ "logging_event_level": "Der Log-Level, f\u00fcr den Sentry ein Ereignis registrieren wird",
+ "logging_level": "Der Log-Level, f\u00fcr den Sentry Protokolle als Breadcrums aufzeichnen wird",
+ "tracing": "Aktivieren des Leistungs-Tracings",
+ "tracing_sample_rate": "Tracing Abtastrate; zwischen 0,0 und 1,0 (1,0 = 100 %)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/translations/he.json b/homeassistant/components/sentry/translations/he.json
new file mode 100644
index 00000000000..3383895686e
--- /dev/null
+++ b/homeassistant/components/sentry/translations/he.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea."
+ },
+ "error": {
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "dsn": "DSN"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json
index 7c4ea22497c..9a0287b2132 100644
--- a/homeassistant/components/seven_segments/manifest.json
+++ b/homeassistant/components/seven_segments/manifest.json
@@ -2,7 +2,7 @@
"domain": "seven_segments",
"name": "Seven Segments OCR",
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
- "requirements": ["pillow==8.1.2"],
+ "requirements": ["pillow==8.2.0"],
"codeowners": ["@fabaff"],
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json
index 6f0ed4c8a9d..15a94a4230f 100644
--- a/homeassistant/components/seventeentrack/manifest.json
+++ b/homeassistant/components/seventeentrack/manifest.json
@@ -3,6 +3,6 @@
"name": "17TRACK",
"documentation": "https://www.home-assistant.io/integrations/seventeentrack",
"requirements": ["py17track==3.2.1"],
- "codeowners": ["@bachya"],
+ "codeowners": [],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/sharkiq/translations/he.json b/homeassistant/components/sharkiq/translations/he.json
new file mode 100644
index 00000000000..2ab1819698c
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/he.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py
index 3f75e290362..425ff11399b 100644
--- a/homeassistant/components/shelly/__init__.py
+++ b/homeassistant/components/shelly/__init__.py
@@ -66,7 +66,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Shelly from a config entry."""
# The custom component for Shelly devices uses shelly domain as well as core
# integration. If the user removes the custom component but doesn't remove the
diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py
index 18f13479c30..dc2dba654f3 100644
--- a/homeassistant/components/shelly/cover.py
+++ b/homeassistant/components/shelly/cover.py
@@ -31,6 +31,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class ShellyCover(ShellyBlockEntity, CoverEntity):
"""Switch that controls a cover block on Shelly devices."""
+ _attr_device_class = DEVICE_CLASS_SHUTTER
+
def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None:
"""Initialize light."""
super().__init__(wrapper, block)
@@ -76,11 +78,6 @@ class ShellyCover(ShellyBlockEntity, CoverEntity):
"""Flag supported features."""
return self._supported_features
- @property
- def device_class(self) -> str:
- """Return the class of the device."""
- return DEVICE_CLASS_SHUTTER
-
async def async_close_cover(self, **kwargs):
"""Close cover."""
self.control_result = await self.set_state(go="close")
diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py
index 05f806dd8e8..e767f49bcbb 100644
--- a/homeassistant/components/shelly/device_trigger.py
+++ b/homeassistant/components/shelly/device_trigger.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.device_automation.exceptions import (
InvalidDeviceAutomationConfig,
)
@@ -33,7 +33,7 @@ from .const import (
)
from .utils import get_device_wrapper, get_input_triggers
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): vol.In(SUPPORTED_INPUTS_EVENTS_TYPES),
vol.Required(CONF_SUBTYPE): vol.In(INPUTS_EVENTS_SUBTYPES),
diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json
index 7e7cdb89f66..70053a86144 100644
--- a/homeassistant/components/shelly/translations/de.json
+++ b/homeassistant/components/shelly/translations/de.json
@@ -12,7 +12,7 @@
"flow_title": "Shelly: {name}",
"step": {
"confirm_discovery": {
- "description": "M\u00f6chten Sie das {Modell} bei {Host} einrichten?\n\nBatteriebetriebene Ger\u00e4te, die passwortgesch\u00fctzt sind, m\u00fcssen aufgeweckt werden, bevor Sie mit dem Einrichten fortfahren.\nBatteriebetriebene Ger\u00e4te, die nicht passwortgesch\u00fctzt sind, werden hinzugef\u00fcgt, wenn das Ger\u00e4t aufwacht. Sie k\u00f6nnen das Ger\u00e4t nun manuell \u00fcber eine Taste am Ger\u00e4t aufwecken oder auf das n\u00e4chste Datenupdate des Ger\u00e4ts warten."
+ "description": "M\u00f6chten Sie das {model} bei {host} einrichten?\n\nBatteriebetriebene Ger\u00e4te, die passwortgesch\u00fctzt sind, m\u00fcssen aufgeweckt werden, bevor Sie mit dem Einrichten fortfahren.\nBatteriebetriebene Ger\u00e4te, die nicht passwortgesch\u00fctzt sind, werden hinzugef\u00fcgt, wenn das Ger\u00e4t aufwacht. Sie k\u00f6nnen das Ger\u00e4t nun manuell \u00fcber eine Taste am Ger\u00e4t aufwecken oder auf das n\u00e4chste Datenupdate des Ger\u00e4ts warten."
},
"credentials": {
"data": {
diff --git a/homeassistant/components/shelly/translations/he.json b/homeassistant/components/shelly/translations/he.json
new file mode 100644
index 00000000000..44d5897f85d
--- /dev/null
+++ b/homeassistant/components/shelly/translations/he.json
@@ -0,0 +1,29 @@
+{
+ "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",
+ "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}",
+ "step": {
+ "confirm_discovery": {
+ "description": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05e8\u05d5\u05e6\u05d4 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} \u05d1-{host}? \n\n\u05d9\u05e9 \u05dc\u05d4\u05e2\u05d9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e2\u05dc\u05d9\u05dd \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d5\u05dc\u05dc\u05d4 \u05d4\u05de\u05d5\u05d2\u05e0\u05d9\u05dd \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d9\u05e1\u05de\u05d4 \u05dc\u05e4\u05e0\u05d9 \u05e9\u05de\u05de\u05e9\u05d9\u05db\u05d9\u05dd \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4.\n\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e2\u05dc\u05d9\u05dd \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d5\u05dc\u05dc\u05d4 \u05e9\u05d0\u05d9\u05e0\u05dd \u05de\u05d5\u05d2\u05e0\u05d9\u05dd \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d9\u05e1\u05de\u05d4 \u05d9\u05ea\u05d5\u05d5\u05e1\u05e4\u05d5 \u05db\u05d0\u05e9\u05e8 \u05d4\u05d4\u05ea\u05e7\u05df \u05d9\u05ea\u05e2\u05d5\u05e8\u05e8, \u05db\u05e2\u05ea \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05e2\u05d9\u05e8 \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05d7\u05db\u05d5\u05ea \u05dc\u05e2\u05d3\u05db\u05d5\u05df \u05d4\u05e0\u05ea\u05d5\u05e0\u05d9\u05dd \u05d4\u05d1\u05d0 \u05de\u05d4\u05d4\u05ea\u05e7\u05df."
+ },
+ "credentials": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shopping_list/translations/he.json b/homeassistant/components/shopping_list/translations/he.json
new file mode 100644
index 00000000000..f245ae2362b
--- /dev/null
+++ b/homeassistant/components/shopping_list/translations/he.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "step": {
+ "user": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05e7\u05d1\u05d5\u05e2 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05e8\u05e9\u05d9\u05de\u05ea \u05d4\u05e7\u05e0\u05d9\u05d5\u05ea?",
+ "title": "\u05e8\u05e9\u05d9\u05de\u05ea \u05e7\u05e0\u05d9\u05d5\u05ea"
+ }
+ }
+ },
+ "title": "\u05e8\u05e9\u05d9\u05de\u05ea \u05e7\u05e0\u05d9\u05d5\u05ea"
+}
\ No newline at end of file
diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py
index fe5b95b639e..5a6a4f6f55c 100644
--- a/homeassistant/components/sia/alarm_control_panel.py
+++ b/homeassistant/components/sia/alarm_control_panel.py
@@ -9,7 +9,6 @@ from pysiaalarm import SIAEvent
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
- CONF_PORT,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_NIGHT,
@@ -17,25 +16,12 @@ from homeassistant.const import (
STATE_ALARM_TRIGGERED,
STATE_UNAVAILABLE,
)
-from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.event import async_call_later
-from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import StateType
-from .const import (
- CONF_ACCOUNT,
- CONF_ACCOUNTS,
- CONF_PING_INTERVAL,
- CONF_ZONES,
- DOMAIN,
- SIA_EVENT,
- SIA_NAME_FORMAT,
- SIA_UNIQUE_ID_FORMAT_ALARM,
-)
-from .utils import get_attr_from_sia_event, get_unavailability_interval
+from .const import CONF_ACCOUNT, CONF_ACCOUNTS, CONF_ZONES, SIA_UNIQUE_ID_FORMAT_ALARM
+from .sia_entity_base import SIABaseEntity
_LOGGER = logging.getLogger(__name__)
@@ -86,7 +72,7 @@ async def async_setup_entry(
)
-class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity):
+class SIAAlarmControlPanel(SIABaseEntity, AlarmControlPanelEntity):
"""Class for SIA Alarm Control Panels."""
def __init__(
@@ -96,138 +82,31 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity):
zone: int,
) -> None:
"""Create SIAAlarmControlPanel object."""
- self._entry: ConfigEntry = entry
- self._account_data: dict[str, Any] = account_data
- self._zone: int = zone
-
- self._port: int = self._entry.data[CONF_PORT]
- self._account: str = self._account_data[CONF_ACCOUNT]
- self._ping_interval: int = self._account_data[CONF_PING_INTERVAL]
-
- self._attr: dict[str, Any] = {}
-
- self._available: bool = True
- self._state: StateType = None
+ super().__init__(entry, account_data, zone, DEVICE_CLASS_ALARM)
+ self._attr_state: StateType = None
self._old_state: StateType = None
- self._cancel_availability_cb: CALLBACK_TYPE | None = None
- async def async_added_to_hass(self) -> None:
- """Run when entity about to be added to hass.
-
- Overridden from Entity.
-
- 1. register the dispatcher and add the callback to on_remove
- 2. get previous state from storage
- 3. if previous state: restore
- 4. if previous state is unavailable: set _available to False and return
- 5. if available: create availability cb
- """
- self.async_on_remove(
- async_dispatcher_connect(
- self.hass,
- SIA_EVENT.format(self._port, self._account),
- self.async_handle_event,
- )
- )
- last_state = await self.async_get_last_state()
- if last_state is not None:
- self._state = last_state.state
- if self.state == STATE_UNAVAILABLE:
- self._available = False
- return
- self._cancel_availability_cb = self.async_create_availability_cb()
-
- async def async_will_remove_from_hass(self) -> None:
- """Run when entity will be removed from hass.
-
- Overridden from Entity.
- """
- if self._cancel_availability_cb:
- self._cancel_availability_cb()
-
- async def async_handle_event(self, sia_event: SIAEvent) -> None:
- """Listen to dispatcher events for this port and account and update state and attributes.
-
- If the port and account combo receives any message it means it is online and can therefore be set to available.
- """
- _LOGGER.debug("Received event: %s", sia_event)
- if int(sia_event.ri) == self._zone:
- self._attr.update(get_attr_from_sia_event(sia_event))
- new_state = CODE_CONSEQUENCES.get(sia_event.code, None)
- if new_state is not None:
- if new_state == PREVIOUS_STATE:
- new_state = self._old_state
- self._state, self._old_state = new_state, self._state
- self._available = True
- self.async_write_ha_state()
- self.async_reset_availability_cb()
-
- @callback
- def async_reset_availability_cb(self) -> None:
- """Reset availability cb by cancelling the current and creating a new one."""
- if self._cancel_availability_cb:
- self._cancel_availability_cb()
- self._cancel_availability_cb = self.async_create_availability_cb()
-
- @callback
- def async_create_availability_cb(self) -> CALLBACK_TYPE:
- """Create a availability cb and return the callback."""
- return async_call_later(
- self.hass,
- get_unavailability_interval(self._ping_interval),
- self.async_set_unavailable,
- )
-
- @callback
- def async_set_unavailable(self, _) -> None:
- """Set unavailable."""
- self._available = False
- self.async_write_ha_state()
-
- @property
- def state(self) -> StateType:
- """Get state."""
- return self._state
-
- @property
- def name(self) -> str:
- """Get Name."""
- return SIA_NAME_FORMAT.format(
- self._port, self._account, self._zone, DEVICE_CLASS_ALARM
- )
-
- @property
- def unique_id(self) -> str:
- """Get unique_id."""
- return SIA_UNIQUE_ID_FORMAT_ALARM.format(
+ self._attr_unique_id = SIA_UNIQUE_ID_FORMAT_ALARM.format(
self._entry.entry_id, self._account, self._zone
)
- @property
- def available(self) -> bool:
- """Get availability."""
- return self._available
+ def update_state(self, sia_event: SIAEvent) -> None:
+ """Update the state of the alarm control panel."""
+ new_state = CODE_CONSEQUENCES.get(sia_event.code, None)
+ if new_state is not None:
+ _LOGGER.debug("New state will be %s", new_state)
+ if new_state == PREVIOUS_STATE:
+ new_state = self._old_state
+ self._attr_state, self._old_state = new_state, self._attr_state
- @property
- def extra_state_attributes(self) -> dict[str, Any]:
- """Return device attributes."""
- return self._attr
-
- @property
- def should_poll(self) -> bool:
- """Return False if entity pushes its state to HA."""
- return False
+ def handle_last_state(self, last_state: State | None) -> None:
+ """Handle the last state."""
+ if last_state is not None:
+ self._attr_state = last_state.state
+ if self.state == STATE_UNAVAILABLE:
+ self._attr_available = False
@property
def supported_features(self) -> int:
- """Flag supported features."""
+ """Return the list of supported features."""
return 0
-
- @property
- def device_info(self) -> DeviceInfo:
- """Return the device_info."""
- return {
- "identifiers": {(DOMAIN, self.unique_id)},
- "name": self.name,
- "via_device": (DOMAIN, f"{self._port}_{self._account}"),
- }
diff --git a/homeassistant/components/sia/binary_sensor.py b/homeassistant/components/sia/binary_sensor.py
new file mode 100644
index 00000000000..eec4f9b2717
--- /dev/null
+++ b/homeassistant/components/sia/binary_sensor.py
@@ -0,0 +1,163 @@
+"""Module for SIA Binary Sensors."""
+from __future__ import annotations
+
+from collections.abc import Iterable
+import logging
+from typing import Any
+
+from pysiaalarm import SIAEvent
+
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOISTURE,
+ DEVICE_CLASS_POWER,
+ DEVICE_CLASS_SMOKE,
+ BinarySensorEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
+from homeassistant.core import HomeAssistant, State
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from .const import (
+ CONF_ACCOUNT,
+ CONF_ACCOUNTS,
+ CONF_ZONES,
+ SIA_HUB_ZONE,
+ SIA_UNIQUE_ID_FORMAT_BINARY,
+)
+from .sia_entity_base import SIABaseEntity
+
+_LOGGER = logging.getLogger(__name__)
+
+
+POWER_CODE_CONSEQUENCES: dict[str, bool] = {
+ "AT": False,
+ "AR": True,
+}
+
+SMOKE_CODE_CONSEQUENCES: dict[str, bool] = {
+ "GA": True,
+ "GH": False,
+ "FA": True,
+ "FH": False,
+ "KA": True,
+ "KH": False,
+}
+
+MOISTURE_CODE_CONSEQUENCES: dict[str, bool] = {
+ "WA": True,
+ "WH": False,
+}
+
+
+def generate_binary_sensors(entry) -> Iterable[SIABinarySensorBase]:
+ """Generate binary sensors.
+
+ For each Account there is one power sensor with zone == 0.
+ For each Zone in each Account there is one smoke and one moisture sensor.
+ """
+ for account in entry.data[CONF_ACCOUNTS]:
+ yield SIABinarySensorPower(entry, account)
+ zones = entry.options[CONF_ACCOUNTS][account[CONF_ACCOUNT]][CONF_ZONES]
+ for zone in range(1, zones + 1):
+ yield SIABinarySensorSmoke(entry, account, zone)
+ yield SIABinarySensorMoisture(entry, account, zone)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up SIA binary sensors from a config entry."""
+ async_add_entities(generate_binary_sensors(entry))
+
+
+class SIABinarySensorBase(SIABaseEntity, BinarySensorEntity):
+ """Class for SIA Binary Sensors."""
+
+ def __init__(
+ self,
+ entry: ConfigEntry,
+ account_data: dict[str, Any],
+ zone: int,
+ device_class: str,
+ ) -> None:
+ """Initialize a base binary sensor."""
+ super().__init__(entry, account_data, zone, device_class)
+
+ self._attr_unique_id = SIA_UNIQUE_ID_FORMAT_BINARY.format(
+ self._entry.entry_id, self._account, self._zone, self._attr_device_class
+ )
+
+ def handle_last_state(self, last_state: State | None) -> None:
+ """Handle the last state."""
+ if last_state is not None and last_state.state is not None:
+ if last_state.state == STATE_ON:
+ self._attr_is_on = True
+ elif last_state.state == STATE_OFF:
+ self._attr_is_on = False
+ elif last_state.state == STATE_UNAVAILABLE:
+ self._attr_available = False
+
+
+class SIABinarySensorMoisture(SIABinarySensorBase):
+ """Class for Moisture Binary Sensors."""
+
+ def __init__(
+ self,
+ entry: ConfigEntry,
+ account_data: dict[str, Any],
+ zone: int,
+ ) -> None:
+ """Initialize a Moisture binary sensor."""
+ super().__init__(entry, account_data, zone, DEVICE_CLASS_MOISTURE)
+ self._attr_entity_registry_enabled_default = False
+
+ def update_state(self, sia_event: SIAEvent) -> None:
+ """Update the state of the binary sensor."""
+ new_state = MOISTURE_CODE_CONSEQUENCES.get(sia_event.code, None)
+ if new_state is not None:
+ _LOGGER.debug("New state will be %s", new_state)
+ self._attr_is_on = new_state
+
+
+class SIABinarySensorSmoke(SIABinarySensorBase):
+ """Class for Smoke Binary Sensors."""
+
+ def __init__(
+ self,
+ entry: ConfigEntry,
+ account_data: dict[str, Any],
+ zone: int,
+ ) -> None:
+ """Initialize a Smoke binary sensor."""
+ super().__init__(entry, account_data, zone, DEVICE_CLASS_SMOKE)
+ self._attr_entity_registry_enabled_default = False
+
+ def update_state(self, sia_event: SIAEvent) -> None:
+ """Update the state of the binary sensor."""
+ new_state = SMOKE_CODE_CONSEQUENCES.get(sia_event.code, None)
+ if new_state is not None:
+ _LOGGER.debug("New state will be %s", new_state)
+ self._attr_is_on = new_state
+
+
+class SIABinarySensorPower(SIABinarySensorBase):
+ """Class for Power Binary Sensors."""
+
+ def __init__(
+ self,
+ entry: ConfigEntry,
+ account_data: dict[str, Any],
+ ) -> None:
+ """Initialize a Power binary sensor."""
+ super().__init__(entry, account_data, SIA_HUB_ZONE, DEVICE_CLASS_POWER)
+ self._attr_entity_registry_enabled_default = True
+
+ def update_state(self, sia_event: SIAEvent) -> None:
+ """Update the state of the binary sensor."""
+ new_state = POWER_CODE_CONSEQUENCES.get(sia_event.code, None)
+ if new_state is not None:
+ _LOGGER.debug("New state will be %s", new_state)
+ self._attr_is_on = new_state
diff --git a/homeassistant/components/sia/const.py b/homeassistant/components/sia/const.py
index 916cdb9621c..711c070b1ee 100644
--- a/homeassistant/components/sia/const.py
+++ b/homeassistant/components/sia/const.py
@@ -2,13 +2,14 @@
from homeassistant.components.alarm_control_panel import (
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
)
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
-PLATFORMS = [ALARM_CONTROL_PANEL_DOMAIN]
+PLATFORMS = [ALARM_CONTROL_PANEL_DOMAIN, BINARY_SENSOR_DOMAIN]
DOMAIN = "sia"
ATTR_CODE = "last_code"
-ATTR_ZONE = "zone"
+ATTR_ZONE = "last_zone"
ATTR_MESSAGE = "last_message"
ATTR_ID = "last_id"
ATTR_TIMESTAMP = "last_timestamp"
@@ -24,5 +25,7 @@ CONF_ZONES = "zones"
SIA_NAME_FORMAT = "{} - {} - zone {} - {}"
SIA_UNIQUE_ID_FORMAT_ALARM = "{}_{}_{}"
+SIA_UNIQUE_ID_FORMAT_BINARY = "{}_{}_{}_{}"
+SIA_HUB_ZONE = 0
SIA_EVENT = "sia_event_{}_{}"
diff --git a/homeassistant/components/sia/sia_entity_base.py b/homeassistant/components/sia/sia_entity_base.py
new file mode 100644
index 00000000000..5169702e67b
--- /dev/null
+++ b/homeassistant/components/sia/sia_entity_base.py
@@ -0,0 +1,132 @@
+"""Module for SIA Base Entity."""
+from __future__ import annotations
+
+from abc import abstractmethod
+import logging
+from typing import Any
+
+from pysiaalarm import SIAEvent
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_PORT
+from homeassistant.core import CALLBACK_TYPE, State, callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.event import async_call_later
+from homeassistant.helpers.restore_state import RestoreEntity
+
+from .const import CONF_ACCOUNT, CONF_PING_INTERVAL, DOMAIN, SIA_EVENT, SIA_NAME_FORMAT
+from .utils import get_attr_from_sia_event, get_unavailability_interval
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SIABaseEntity(RestoreEntity):
+ """Base class for SIA entities."""
+
+ def __init__(
+ self,
+ entry: ConfigEntry,
+ account_data: dict[str, Any],
+ zone: int,
+ device_class: str,
+ ) -> None:
+ """Create SIABaseEntity object."""
+ self._entry: ConfigEntry = entry
+ self._account_data: dict[str, Any] = account_data
+ self._zone: int = zone
+ self._attr_device_class: str = device_class
+
+ self._port: int = self._entry.data[CONF_PORT]
+ self._account: str = self._account_data[CONF_ACCOUNT]
+ self._ping_interval: int = self._account_data[CONF_PING_INTERVAL]
+
+ self._cancel_availability_cb: CALLBACK_TYPE | None = None
+
+ self._attr_extra_state_attributes: dict[str, Any] = {}
+ self._attr_should_poll = False
+ self._attr_name = SIA_NAME_FORMAT.format(
+ self._port, self._account, self._zone, self._attr_device_class
+ )
+
+ async def async_added_to_hass(self) -> None:
+ """Run when entity about to be added to hass.
+
+ Overridden from Entity.
+
+ 1. register the dispatcher and add the callback to on_remove
+ 2. get previous state from storage and pass to entity specific function
+ 3. if available: create availability cb
+ """
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ SIA_EVENT.format(self._port, self._account),
+ self.async_handle_event,
+ )
+ )
+ self.handle_last_state(await self.async_get_last_state())
+ if self._attr_available:
+ self.async_create_availability_cb()
+
+ @abstractmethod
+ def handle_last_state(self, last_state: State | None) -> None:
+ """Handle the last state."""
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Run when entity will be removed from hass.
+
+ Overridden from Entity.
+ """
+ if self._cancel_availability_cb:
+ self._cancel_availability_cb()
+
+ @callback
+ def async_handle_event(self, sia_event: SIAEvent) -> None:
+ """Listen to dispatcher events for this port and account and update state and attributes.
+
+ If the port and account combo receives any message it means it is online and can therefore be set to available.
+ """
+ _LOGGER.debug("Received event: %s", sia_event)
+ if int(sia_event.ri) == self._zone:
+ self._attr_extra_state_attributes.update(get_attr_from_sia_event(sia_event))
+ self.update_state(sia_event)
+ self.async_reset_availability_cb()
+ self.async_write_ha_state()
+
+ @abstractmethod
+ def update_state(self, sia_event: SIAEvent) -> None:
+ """Do the entity specific state updates."""
+
+ @callback
+ def async_reset_availability_cb(self) -> None:
+ """Reset availability cb by cancelling the current and creating a new one."""
+ self._attr_available = True
+ if self._cancel_availability_cb:
+ self._cancel_availability_cb()
+ self.async_create_availability_cb()
+
+ def async_create_availability_cb(self) -> None:
+ """Create a availability cb and return the callback."""
+ self._cancel_availability_cb = async_call_later(
+ self.hass,
+ get_unavailability_interval(self._ping_interval),
+ self.async_set_unavailable,
+ )
+
+ @callback
+ def async_set_unavailable(self, _) -> None:
+ """Set unavailable."""
+ self._attr_available = False
+ self.async_write_ha_state()
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return the device_info."""
+ assert self._attr_name is not None
+ assert self.unique_id is not None
+ return {
+ "name": self._attr_name,
+ "identifiers": {(DOMAIN, self.unique_id)},
+ "via_device": (DOMAIN, f"{self._port}_{self._account}"),
+ }
diff --git a/homeassistant/components/sia/translations/ca.json b/homeassistant/components/sia/translations/ca.json
new file mode 100644
index 00000000000..904d1542c8f
--- /dev/null
+++ b/homeassistant/components/sia/translations/ca.json
@@ -0,0 +1,50 @@
+{
+ "config": {
+ "error": {
+ "invalid_account_format": "El compte no \u00e9s un valor hexadecimal, utilitza nom\u00e9s 0-9 i A-F.",
+ "invalid_account_length": "El compte no t\u00e9 la longitud correcta, ha de tenir entre 3 i 16 car\u00e0cters.",
+ "invalid_key_format": "La clau no \u00e9s un valor hexadecimal, utilitza nom\u00e9s 0-9 i A-F.",
+ "invalid_key_length": "La clau no t\u00e9 longitud correcta, ha de tenir 16, 24 o 32 car\u00e0cters hexadecimals.",
+ "invalid_ping": "L'interval de refresc ha d'estar compr\u00e8s entre 1 i 1440 minuts.",
+ "invalid_zones": "Cal que hi hagi com a m\u00ednim 1 zona.",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "additional_account": {
+ "data": {
+ "account": "ID del compte",
+ "additional_account": "Comptes addicionals",
+ "encryption_key": "Clau de xifrat",
+ "ping_interval": "Interval de refresc (min)",
+ "zones": "Nombre de zones del compte"
+ },
+ "title": "Afegeix un altre compte al port actual."
+ },
+ "user": {
+ "data": {
+ "account": "ID del compte",
+ "additional_account": "Comptes addicionals",
+ "encryption_key": "Clau de xifrat",
+ "ping_interval": "Interval de refresc (min)",
+ "port": "Port",
+ "protocol": "Protocol",
+ "zones": "Nombre de zones del compte"
+ },
+ "title": "Crea una connexi\u00f3 per a sistemes d'alarma basats en SIA."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "options": {
+ "data": {
+ "ignore_timestamps": "Ignora la comprovaci\u00f3 de marca de temps dels esdeveniments SIA",
+ "zones": "Nombre de zones del compte"
+ },
+ "description": "Configura les opcions del compte: {account}",
+ "title": "Opcions de configuraci\u00f3 de SIA."
+ }
+ }
+ },
+ "title": "SIA Alarm Systems"
+}
\ No newline at end of file
diff --git a/homeassistant/components/sia/translations/de.json b/homeassistant/components/sia/translations/de.json
new file mode 100644
index 00000000000..6da5a2c4750
--- /dev/null
+++ b/homeassistant/components/sia/translations/de.json
@@ -0,0 +1,50 @@
+{
+ "config": {
+ "error": {
+ "invalid_account_format": "Das Konto ist kein Hex-Wert. Bitte verwenden Sie nur 0-9 und A-F.",
+ "invalid_account_length": "Das Konto hat nicht die richtige L\u00e4nge. Es muss zwischen 3 und 16 Zeichen lang sein.",
+ "invalid_key_format": "Der Schl\u00fcssel ist kein Hex-Wert, bitte verwenden Sie nur 0-9 und A-F.",
+ "invalid_key_length": "Der Schl\u00fcssel hat nicht die richtige L\u00e4nge. Er muss 16, 24 oder 32 Hex-Zeichen lang sein.",
+ "invalid_ping": "Das Ping-Intervall muss zwischen 1 und 1440 Minuten liegen.",
+ "invalid_zones": "Es muss mindestens eine Zone vorhanden sein.",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "additional_account": {
+ "data": {
+ "account": "Konto-ID",
+ "additional_account": "Zus\u00e4tzliche Konten",
+ "encryption_key": "Verschl\u00fcsselungscode",
+ "ping_interval": "Ping-Intervall (min)",
+ "zones": "Anzahl an Zonen f\u00fcr das Konto"
+ },
+ "title": "Dem aktuellen Port ein weiteres Konto hinzuf\u00fcgen."
+ },
+ "user": {
+ "data": {
+ "account": "Konto-ID",
+ "additional_account": "Zus\u00e4tzliche Konten",
+ "encryption_key": "Verschl\u00fcsselungscode",
+ "ping_interval": "Ping-Intervall (min)",
+ "port": "Port",
+ "protocol": "Protokoll",
+ "zones": "Anzahl an Zonen f\u00fcr das Konto"
+ },
+ "title": "Erstellen einer Verbindung f\u00fcr SIA-basierte Alarmsysteme."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "options": {
+ "data": {
+ "ignore_timestamps": "Ignorieren der Zeitstempelpr\u00fcfung der SIA-Ereignisse",
+ "zones": "Anzahl an Zonen f\u00fcr das Konto"
+ },
+ "description": "Stellen Sie die Optionen f\u00fcr das Konto {account} ein:",
+ "title": "Optionen f\u00fcr das SIA-Setup."
+ }
+ }
+ },
+ "title": "SIA Alarmsysteme"
+}
\ No newline at end of file
diff --git a/homeassistant/components/sia/translations/en.json b/homeassistant/components/sia/translations/en.json
index ff6781669dd..50a00a4bf23 100644
--- a/homeassistant/components/sia/translations/en.json
+++ b/homeassistant/components/sia/translations/en.json
@@ -4,7 +4,7 @@
"invalid_account_format": "The account is not a hex value, please use only 0-9 and A-F.",
"invalid_account_length": "The account is not the right length, it has to be between 3 and 16 characters.",
"invalid_key_format": "The key is not a hex value, please use only 0-9 and A-F.",
- "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 characters hex characters.",
+ "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 hex characters.",
"invalid_ping": "The ping interval needs to be between 1 and 1440 minutes.",
"invalid_zones": "There needs to be at least 1 zone.",
"unknown": "Unexpected error"
diff --git a/homeassistant/components/sia/translations/es.json b/homeassistant/components/sia/translations/es.json
new file mode 100644
index 00000000000..f32b6a86626
--- /dev/null
+++ b/homeassistant/components/sia/translations/es.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "error": {
+ "invalid_account_format": "La cuenta no es un valor hexadecimal, por favor utilice s\u00f3lo 0-9 y A-F.",
+ "invalid_account_length": "La cuenta no tiene la longitud adecuada, tiene que tener entre 3 y 16 caracteres.",
+ "invalid_key_format": "La clave no es un valor hexadecimal, por favor utilice s\u00f3lo 0-9 y A-F.",
+ "invalid_key_length": "La clave no tiene la longitud correcta, tiene que ser de 16, 24 o 32 caracteres hexadecimales.",
+ "invalid_ping": "El intervalo de ping debe estar entre 1 y 1440 minutos.",
+ "invalid_zones": "Tiene que haber al menos 1 zona."
+ },
+ "step": {
+ "additional_account": {
+ "title": "Agrega otra cuenta al puerto actual."
+ },
+ "user": {
+ "data": {
+ "account": "ID de la cuenta",
+ "additional_account": "Cuentas adicionales",
+ "encryption_key": "Clave de encriptaci\u00f3n",
+ "ping_interval": "Intervalo de ping (min)",
+ "protocol": "Protocolo",
+ "zones": "N\u00famero de zonas de la cuenta"
+ },
+ "title": "Cree una conexi\u00f3n para sistemas de alarma basados en SIA."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "options": {
+ "data": {
+ "ignore_timestamps": "Ignore la verificaci\u00f3n de la marca de tiempo de los eventos SIA"
+ },
+ "description": "Configure las opciones para la cuenta: {account}",
+ "title": "Opciones para la configuraci\u00f3n de SIA."
+ }
+ }
+ },
+ "title": "Sistemas de alarma SIA"
+}
\ No newline at end of file
diff --git a/homeassistant/components/sia/translations/he.json b/homeassistant/components/sia/translations/he.json
new file mode 100644
index 00000000000..65f845f99c1
--- /dev/null
+++ b/homeassistant/components/sia/translations/he.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "error": {
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "additional_account": {
+ "data": {
+ "account": "\u05de\u05d6\u05d4\u05d4 \u05d7\u05e9\u05d1\u05d5\u05df",
+ "additional_account": "\u05d7\u05e9\u05d1\u05d5\u05e0\u05d5\u05ea \u05e0\u05d5\u05e1\u05e4\u05d9\u05dd",
+ "encryption_key": "\u05de\u05e4\u05ea\u05d7 \u05d4\u05e6\u05e4\u05e0\u05d4",
+ "ping_interval": "\u05de\u05e8\u05d5\u05d5\u05d7 \u05d6\u05de\u05df \u05dc\u05e4\u05d9\u05e0\u05d2 (\u05de\u05d9\u05e0\u05d9\u05de\u05d5\u05dd)",
+ "zones": "\u05de\u05e1\u05e4\u05e8 \u05d4\u05d0\u05d6\u05d5\u05e8\u05d9\u05dd \u05dc\u05d7\u05e9\u05d1\u05d5\u05df"
+ }
+ },
+ "user": {
+ "data": {
+ "account": "\u05de\u05d6\u05d4\u05d4 \u05d7\u05e9\u05d1\u05d5\u05df",
+ "additional_account": "\u05d7\u05e9\u05d1\u05d5\u05e0\u05d5\u05ea \u05e0\u05d5\u05e1\u05e4\u05d9\u05dd",
+ "encryption_key": "\u05de\u05e4\u05ea\u05d7 \u05d4\u05e6\u05e4\u05e0\u05d4",
+ "ping_interval": "\u05de\u05e8\u05d5\u05d5\u05d7 \u05d6\u05de\u05df \u05dc\u05e4\u05d9\u05e0\u05d2 (\u05de\u05d9\u05e0\u05d9\u05de\u05d5\u05dd)",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc",
+ "zones": "\u05de\u05e1\u05e4\u05e8 \u05d4\u05d0\u05d6\u05d5\u05e8\u05d9\u05dd \u05dc\u05d7\u05e9\u05d1\u05d5\u05df"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "options": {
+ "data": {
+ "zones": "\u05de\u05e1\u05e4\u05e8 \u05d4\u05d0\u05d6\u05d5\u05e8\u05d9\u05dd \u05dc\u05d7\u05e9\u05d1\u05d5\u05df"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sia/translations/hu.json b/homeassistant/components/sia/translations/hu.json
new file mode 100644
index 00000000000..f5538bfd6b5
--- /dev/null
+++ b/homeassistant/components/sia/translations/hu.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "error": {
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port",
+ "protocol": "Protokoll"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sia/translations/it.json b/homeassistant/components/sia/translations/it.json
new file mode 100644
index 00000000000..7806cd0bed0
--- /dev/null
+++ b/homeassistant/components/sia/translations/it.json
@@ -0,0 +1,50 @@
+{
+ "config": {
+ "error": {
+ "invalid_account_format": "L'account non \u00e8 un valore esadecimale, utilizza solo 0-9 e AF.",
+ "invalid_account_length": "L'account non \u00e8 della lunghezza giusta, deve essere compreso tra 3 e 16 caratteri.",
+ "invalid_key_format": "La chiave non \u00e8 un valore esadecimale, utilizza solo 0-9 e AF.",
+ "invalid_key_length": "La chiave non \u00e8 della lunghezza corretta, deve essere di 16, 24 o 32 caratteri esadecimali.",
+ "invalid_ping": "L'intervallo di ping deve essere compreso tra 1 e 1440 minuti.",
+ "invalid_zones": "Deve essere presente almeno 1 zona.",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "additional_account": {
+ "data": {
+ "account": "ID account",
+ "additional_account": "Account aggiuntivi",
+ "encryption_key": "Chiave di crittografia",
+ "ping_interval": "Intervallo ping (min)",
+ "zones": "Numero di zone per l'account"
+ },
+ "title": "Aggiungi un altro account alla porta corrente."
+ },
+ "user": {
+ "data": {
+ "account": "ID account",
+ "additional_account": "Account aggiuntivi",
+ "encryption_key": "Chiave di crittografia",
+ "ping_interval": "Intervallo ping (min)",
+ "port": "Porta",
+ "protocol": "Protocollo",
+ "zones": "Numero di zone per l'account"
+ },
+ "title": "Creare una connessione per i sistemi di allarme basati su SIA."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "options": {
+ "data": {
+ "ignore_timestamps": "Ignora il controllo del timestamp degli eventi SIA",
+ "zones": "Numero di zone per l'account"
+ },
+ "description": "Imposta le opzioni per l'account: {account}",
+ "title": "Opzioni per l'impostazione SIA."
+ }
+ }
+ },
+ "title": "Sistemi di allarme SIA"
+}
\ No newline at end of file
diff --git a/homeassistant/components/sia/translations/nl.json b/homeassistant/components/sia/translations/nl.json
index 789106ead23..8afc0b88651 100644
--- a/homeassistant/components/sia/translations/nl.json
+++ b/homeassistant/components/sia/translations/nl.json
@@ -1,6 +1,12 @@
{
"config": {
"error": {
+ "invalid_account_format": "Het account is geen hex waarde, gebruik alleen 0-9 en A-F.",
+ "invalid_account_length": "Het account heeft niet de juiste lengte, het moet tussen de 3 en 16 karakters zijn.",
+ "invalid_key_format": "De sleutel is geen hex waarde, gebruik alleen 0-9 en A-F.",
+ "invalid_key_length": "De sleutel heeft niet de juiste lengte, het moeten 16, 24 of 32 hex karakters zijn.",
+ "invalid_ping": "Het ping-interval moet tussen 1 en 1440 minuten liggen.",
+ "invalid_zones": "Er moet minstens 1 zone zijn.",
"unknown": "Onverwachte fout"
},
"step": {
diff --git a/homeassistant/components/sia/translations/no.json b/homeassistant/components/sia/translations/no.json
index cd09ae7cdff..61ce61b7ee5 100644
--- a/homeassistant/components/sia/translations/no.json
+++ b/homeassistant/components/sia/translations/no.json
@@ -4,7 +4,7 @@
"invalid_account_format": "Kontoen er ikke en hex-verdi. Bruk bare 0-9 og AF.",
"invalid_account_length": "Kontoen har ikke riktig lengde, den m\u00e5 v\u00e6re mellom 3 og 16 tegn.",
"invalid_key_format": "N\u00f8kkelen er ikke en hex-verdi, bruk bare 0-9 og AF.",
- "invalid_key_length": "N\u00f8kkelen har ikke riktig lengde, den m\u00e5 v\u00e6re p\u00e5 16, 24 eller 32 tegn med hex-tegn.",
+ "invalid_key_length": "N\u00f8kkelen har ikke riktig lengde, den m\u00e5 ha 16, 24 eller 32 hekser.",
"invalid_ping": "Ping-intervallet m\u00e5 v\u00e6re mellom 1 og 1440 minutter.",
"invalid_zones": "Det m\u00e5 v\u00e6re minst 1 sone.",
"unknown": "Uventet feil"
diff --git a/homeassistant/components/sia/translations/pl.json b/homeassistant/components/sia/translations/pl.json
new file mode 100644
index 00000000000..d41281519d4
--- /dev/null
+++ b/homeassistant/components/sia/translations/pl.json
@@ -0,0 +1,50 @@
+{
+ "config": {
+ "error": {
+ "invalid_account_format": "Konto nie jest warto\u015bci\u0105 szesnastkow\u0105, u\u017cyj tylko 0-9 i A-F.",
+ "invalid_account_length": "Konto ma niew\u0142a\u015bciw\u0105 d\u0142ugo\u015b\u0107, musi mie\u0107 od 3 do 16 znak\u00f3w.",
+ "invalid_key_format": "Klucz nie jest warto\u015bci\u0105 szesnastkow\u0105, u\u017cyj tylko 0-9 i A-F.",
+ "invalid_key_length": "Klucz ma niew\u0142a\u015bciw\u0105 d\u0142ugo\u015b\u0107, musi mie\u0107 16, 24 lub 32 znaki szesnastkowe.",
+ "invalid_ping": "Cz\u0119stotliwo\u015b\u0107 pingowania musi wynosi\u0107 od 1 do 1440 minut.",
+ "invalid_zones": "Musi istnie\u0107 co najmniej 1 strefa.",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "additional_account": {
+ "data": {
+ "account": "Identyfikator konta",
+ "additional_account": "Dodatkowe konta",
+ "encryption_key": "Klucz szyfrowania",
+ "ping_interval": "Cz\u0119stotliwo\u015b\u0107 pingowania (min)",
+ "zones": "Liczba stref dla konta"
+ },
+ "title": "Dodaj kolejne konto do bie\u017c\u0105cego portu."
+ },
+ "user": {
+ "data": {
+ "account": "Identyfikator konta",
+ "additional_account": "Dodatkowe konta",
+ "encryption_key": "Klucz szyfrowania",
+ "ping_interval": "Cz\u0119stotliwo\u015b\u0107 pingowania (min)",
+ "port": "Port",
+ "protocol": "Protok\u00f3\u0142",
+ "zones": "Liczba stref dla konta"
+ },
+ "title": "Tworzenie po\u0142\u0105czenia dla system\u00f3w alarmowych opartych na SIA."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "options": {
+ "data": {
+ "ignore_timestamps": "Ignoruj sprawdzanie znacznika czasu dla wydarze\u0144 SIA",
+ "zones": "Liczba stref dla konta"
+ },
+ "description": "Ustaw opcje dla konta: {account}",
+ "title": "Opcje dla konfiguracji SIA."
+ }
+ }
+ },
+ "title": "Systemy alarmowe SIA"
+}
\ No newline at end of file
diff --git a/homeassistant/components/sia/translations/zh-Hant.json b/homeassistant/components/sia/translations/zh-Hant.json
index 6ebf56aa049..6cd3c879656 100644
--- a/homeassistant/components/sia/translations/zh-Hant.json
+++ b/homeassistant/components/sia/translations/zh-Hant.json
@@ -4,7 +4,7 @@
"invalid_account_format": "\u5e33\u865f\u70ba\u5341\u516d\u9032\u4f4d\u6578\u503c\u3001\u8acb\u4f7f\u7528 0-9 \u53ca A-F\u3002",
"invalid_account_length": "\u5e33\u865f\u9577\u5ea6\u4e0d\u6b63\u78ba\u3001\u5fc5\u9808\u4ecb\u65bc 3 \u81f3 16 \u500b\u5b57\u5143\u4e4b\u9593\u3002",
"invalid_key_format": "\u5bc6\u9470\u70ba\u5341\u516d\u9032\u4f4d\u6578\u503c\u3001\u8acb\u4f7f\u7528 0-9 \u53ca A-F\u3002",
- "invalid_key_length": "\u5e33\u865f\u9577\u5ea6\u4e0d\u6b63\u78ba\u3001\u5fc5\u9808\u70ba 14\u300124 \u6216 32 \u500b\u5341\u516d\u9032\u4f4d\u5b57\u5143\u3002",
+ "invalid_key_length": "\u5e33\u865f\u9577\u5ea6\u4e0d\u6b63\u78ba\u3001\u5fc5\u9808\u70ba 16\u300124 \u6216 32 \u500b\u5341\u516d\u9032\u4f4d\u5b57\u5143\u3002",
"invalid_ping": "Ping \u9593\u8ddd\u5fc5\u9808\u70ba 1 \u81f3 1440 \u5206\u9418\u4e4b\u9593\u3002",
"invalid_zones": "\u81f3\u5c11\u5fc5\u9808\u6709\u4e00\u500b\u5206\u5340\u3002",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py
index fa636eb757f..e31b30f1174 100644
--- a/homeassistant/components/sighthound/image_processing.py
+++ b/homeassistant/components/sighthound/image_processing.py
@@ -75,6 +75,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class SighthoundEntity(ImageProcessingEntity):
"""Create a sighthound entity."""
+ _attr_unit_of_measurement = ATTR_PEOPLE
+
def __init__(
self, api, camera_entity, name, save_file_folder, save_timestamped_file
):
@@ -164,11 +166,6 @@ class SighthoundEntity(ImageProcessingEntity):
"""Return the state of the entity."""
return self._state
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return ATTR_PEOPLE
-
@property
def extra_state_attributes(self):
"""Return the attributes."""
diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json
index e372c995b5e..b22b645a7e8 100644
--- a/homeassistant/components/sighthound/manifest.json
+++ b/homeassistant/components/sighthound/manifest.json
@@ -2,7 +2,7 @@
"domain": "sighthound",
"name": "Sighthound",
"documentation": "https://www.home-assistant.io/integrations/sighthound",
- "requirements": ["pillow==8.1.2", "simplehound==0.3"],
+ "requirements": ["pillow==8.2.0", "simplehound==0.3"],
"codeowners": ["@robmarkcole"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py
index c49aeb065e4..01e31633a1a 100644
--- a/homeassistant/components/simplisafe/__init__.py
+++ b/homeassistant/components/simplisafe/__init__.py
@@ -2,11 +2,11 @@
import asyncio
from uuid import UUID
-from simplipy import API
+from simplipy import get_api
from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError
import voluptuous as vol
-from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN, CONF_USERNAME
+from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import CoreState, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import (
@@ -107,14 +107,6 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend(
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
-@callback
-def _async_save_refresh_token(hass, config_entry, token):
- """Save a refresh token to the config entry."""
- hass.config_entries.async_update_entry(
- config_entry, data={**config_entry.data, CONF_TOKEN: token}
- )
-
-
async def async_get_client_id(hass):
"""Get a client ID (based on the HASS unique ID) for the SimpliSafe API.
@@ -136,16 +128,15 @@ async def async_register_base_station(hass, system, config_entry_id):
)
-async def async_setup(hass, config):
- """Set up the SimpliSafe component."""
- hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}}
- return True
-
-
async def async_setup_entry(hass, config_entry): # noqa: C901
"""Set up SimpliSafe as config entry."""
+ hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}, DATA_LISTENER: {}})
+ hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = []
hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = []
+ if CONF_PASSWORD not in config_entry.data:
+ raise ConfigEntryAuthFailed("Config schema change requires re-authentication")
+
entry_updates = {}
if not config_entry.unique_id:
# If the config entry doesn't already have a unique ID, set one:
@@ -168,19 +159,19 @@ async def async_setup_entry(hass, config_entry): # noqa: C901
websession = aiohttp_client.async_get_clientsession(hass)
try:
- api = await API.login_via_token(
- config_entry.data[CONF_TOKEN], client_id=client_id, session=websession
+ api = await get_api(
+ config_entry.data[CONF_USERNAME],
+ config_entry.data[CONF_PASSWORD],
+ client_id=client_id,
+ session=websession,
)
- except InvalidCredentialsError:
- LOGGER.error("Invalid credentials provided")
- return False
+ except InvalidCredentialsError as err:
+ raise ConfigEntryAuthFailed from err
except SimplipyError as err:
LOGGER.error("Config entry failed: %s", err)
raise ConfigEntryNotReady from err
- _async_save_refresh_token(hass, config_entry, api.refresh_token)
-
- simplisafe = SimpliSafe(hass, api, config_entry)
+ simplisafe = SimpliSafe(hass, config_entry, api)
try:
await simplisafe.async_init()
@@ -307,10 +298,9 @@ async def async_reload_entry(hass, config_entry):
class SimpliSafe:
"""Define a SimpliSafe data object."""
- def __init__(self, hass, api, config_entry):
+ def __init__(self, hass, config_entry, api):
"""Initialize."""
self._api = api
- self._emergency_refresh_token_used = False
self._hass = hass
self._system_notifications = {}
self.config_entry = config_entry
@@ -387,23 +377,7 @@ class SimpliSafe:
for result in results:
if isinstance(result, InvalidCredentialsError):
- if self._emergency_refresh_token_used:
- raise ConfigEntryAuthFailed(
- "Update failed with stored refresh token"
- )
-
- LOGGER.warning("SimpliSafe cloud error; trying stored refresh token")
- self._emergency_refresh_token_used = True
-
- try:
- await self._api.refresh_access_token(
- self.config_entry.data[CONF_TOKEN]
- )
- return
- except SimplipyError as err:
- raise UpdateFailed( # pylint: disable=raise-missing-from
- f"Error while using stored refresh token: {err}"
- )
+ raise ConfigEntryAuthFailed("Invalid credentials") from result
if isinstance(result, EndpointUnavailable):
# In case the user attempts an action not allowed in their current plan,
@@ -414,16 +388,6 @@ class SimpliSafe:
if isinstance(result, SimplipyError):
raise UpdateFailed(f"SimpliSafe error while updating: {result}")
- if self._api.refresh_token != self.config_entry.data[CONF_TOKEN]:
- _async_save_refresh_token(
- self._hass, self.config_entry, self._api.refresh_token
- )
-
- # If we've reached this point using an emergency refresh token, we're in the
- # clear and we can discard it:
- if self._emergency_refresh_token_used:
- self._emergency_refresh_token_used = False
-
class SimpliSafeEntity(CoordinatorEntity):
"""Define a base SimpliSafe entity."""
diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py
index ba51356f770..ac31779175f 100644
--- a/homeassistant/components/simplisafe/config_flow.py
+++ b/homeassistant/components/simplisafe/config_flow.py
@@ -1,5 +1,5 @@
"""Config flow to configure the SimpliSafe component."""
-from simplipy import API
+from simplipy import get_api
from simplipy.errors import (
InvalidCredentialsError,
PendingAuthorizationError,
@@ -8,7 +8,7 @@ from simplipy.errors import (
import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
+from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
@@ -47,7 +47,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
client_id = await async_get_client_id(self.hass)
websession = aiohttp_client.async_get_clientsession(self.hass)
- return await API.login_via_credentials(
+ return await get_api(
self._username,
self._password,
client_id=client_id,
@@ -59,7 +59,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors = {}
try:
- simplisafe = await self._async_get_simplisafe_api()
+ await self._async_get_simplisafe_api()
except PendingAuthorizationError:
LOGGER.info("Awaiting confirmation of MFA email click")
return await self.async_step_mfa()
@@ -79,7 +79,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_finish(
{
CONF_USERNAME: self._username,
- CONF_TOKEN: simplisafe.refresh_token,
+ CONF_PASSWORD: self._password,
CONF_CODE: self._code,
}
)
@@ -89,6 +89,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
existing_entry = await self.async_set_unique_id(self._username)
if existing_entry:
self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(existing_entry.entry_id)
+ )
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=self._username, data=user_input)
@@ -98,7 +101,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="mfa")
try:
- simplisafe = await self._async_get_simplisafe_api()
+ await self._async_get_simplisafe_api()
except PendingAuthorizationError:
LOGGER.error("Still awaiting confirmation of MFA email click")
return self.async_show_form(
@@ -108,7 +111,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_finish(
{
CONF_USERNAME: self._username,
- CONF_TOKEN: simplisafe.refresh_token,
+ CONF_PASSWORD: self._password,
CONF_CODE: self._code,
}
)
diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json
index 79e11828eaa..eff37bf1548 100644
--- a/homeassistant/components/simplisafe/manifest.json
+++ b/homeassistant/components/simplisafe/manifest.json
@@ -3,7 +3,7 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
- "requirements": ["simplisafe-python==10.0.0"],
+ "requirements": ["simplisafe-python==11.0.0"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json
index ad973261a0e..23f85495025 100644
--- a/homeassistant/components/simplisafe/strings.json
+++ b/homeassistant/components/simplisafe/strings.json
@@ -7,7 +7,7 @@
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
- "description": "Your access token has expired or been revoked. Enter your password to re-link your account.",
+ "description": "Your access has expired or been revoked. Enter your password to re-link your account.",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json
index 5b5d0cf1798..e8bb80d1b88 100644
--- a/homeassistant/components/simplisafe/translations/ca.json
+++ b/homeassistant/components/simplisafe/translations/ca.json
@@ -19,7 +19,7 @@
"data": {
"password": "Contrasenya"
},
- "description": "El token d'acc\u00e9s ha caducat o ha estat revocat. Introdueix la teva contrasenya per tornar a vincular el compte.",
+ "description": "L'acc\u00e9s ha caducat o ha estat revocat. Introdueix la teva contrasenya per tornar a vincular el compte.",
"title": "Reautenticaci\u00f3 de la integraci\u00f3"
},
"user": {
diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json
index b9e274666bb..331eb65ca83 100644
--- a/homeassistant/components/simplisafe/translations/en.json
+++ b/homeassistant/components/simplisafe/translations/en.json
@@ -19,7 +19,7 @@
"data": {
"password": "Password"
},
- "description": "Your access token has expired or been revoked. Enter your password to re-link your account.",
+ "description": "Your access has expired or been revoked. Enter your password to re-link your account.",
"title": "Reauthenticate Integration"
},
"user": {
diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json
index 7b6e317b922..e815785f0b5 100644
--- a/homeassistant/components/simplisafe/translations/et.json
+++ b/homeassistant/components/simplisafe/translations/et.json
@@ -19,7 +19,7 @@
"data": {
"password": "Salas\u00f5na"
},
- "description": "Juurdep\u00e4\u00e4suluba on aegunud v\u00f5i see on t\u00fchistatud. Konto uuesti linkimiseks sisesta oma parool.",
+ "description": "Juurdep\u00e4\u00e4suluba on aegunud v\u00f5i on see t\u00fchistatud. Konto uuesti linkimiseks sisesta oma salas\u00f5na.",
"title": "Taastuvasta SimpliSafe'i konto"
},
"user": {
diff --git a/homeassistant/components/simplisafe/translations/he.json b/homeassistant/components/simplisafe/translations/he.json
index 3007c0e968c..dd3969f269f 100644
--- a/homeassistant/components/simplisafe/translations/he.json
+++ b/homeassistant/components/simplisafe/translations/he.json
@@ -1,9 +1,23 @@
{
"config": {
+ "abort": {
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
"step": {
- "user": {
+ "reauth_confirm": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "description": "\u05ea\u05d5\u05e7\u05e3 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc\u05da \u05e4\u05d2 \u05d0\u05d5 \u05d1\u05d5\u05d8\u05dc. \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05e7\u05e9\u05e8 \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da."
+ },
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05d3\u05d5\u05d0\"\u05dc"
}
}
}
diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json
index b5ce2a26702..13e7fe4562a 100644
--- a/homeassistant/components/simplisafe/translations/it.json
+++ b/homeassistant/components/simplisafe/translations/it.json
@@ -19,7 +19,7 @@
"data": {
"password": "Password"
},
- "description": "Il token di accesso \u00e8 scaduto o \u00e8 stato revocato. Inserisci la tua password per ricollegare il tuo account.",
+ "description": "L'accesso \u00e8 scaduto o revocato. Inserisci la password per ri-collegare il tuo account.",
"title": "Autenticare nuovamente l'integrazione"
},
"user": {
diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json
index 32802248856..bc82715ad63 100644
--- a/homeassistant/components/simplisafe/translations/no.json
+++ b/homeassistant/components/simplisafe/translations/no.json
@@ -19,7 +19,7 @@
"data": {
"password": "Passord"
},
- "description": "Tilgangstokenet ditt har utl\u00f8pt eller blitt tilbakekalt. Skriv inn passordet ditt for \u00e5 koble til kontoen din p\u00e5 nytt.",
+ "description": "Din tilgang har utl\u00f8pt eller blitt tilbakekalt. Skriv inn passordet ditt for \u00e5 koble kontoen din p\u00e5 nytt.",
"title": "Godkjenne integrering p\u00e5 nytt"
},
"user": {
diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json
index 7793d51817a..260d9d6b148 100644
--- a/homeassistant/components/simplisafe/translations/pl.json
+++ b/homeassistant/components/simplisafe/translations/pl.json
@@ -19,7 +19,7 @@
"data": {
"password": "Has\u0142o"
},
- "description": "Tw\u00f3j token dost\u0119pu wygas\u0142 lub zosta\u0142 uniewa\u017cniony. Wprowad\u017a has\u0142o, aby ponownie po\u0142\u0105czy\u0107 swoje konto.",
+ "description": "Tw\u00f3j dost\u0119p wygas\u0142 lub zosta\u0142 uniewa\u017cniony. Wprowad\u017a has\u0142o, aby ponownie po\u0142\u0105czy\u0107 swoje konto.",
"title": "Ponownie uwierzytelnij integracj\u0119"
},
"user": {
diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json
index bcfffc57533..5fc3fce065e 100644
--- a/homeassistant/components/simplisafe/translations/ru.json
+++ b/homeassistant/components/simplisafe/translations/ru.json
@@ -19,7 +19,7 @@
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c"
},
- "description": "\u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0431\u044b\u043b \u0430\u043d\u043d\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c.",
+ "description": "\u0421\u0440\u043e\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0431\u044b\u043b \u0430\u043d\u043d\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c.",
"title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
},
"user": {
diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json
index 27064ed1055..517d48321a8 100644
--- a/homeassistant/components/simplisafe/translations/zh-Hant.json
+++ b/homeassistant/components/simplisafe/translations/zh-Hant.json
@@ -19,7 +19,7 @@
"data": {
"password": "\u5bc6\u78bc"
},
- "description": "\u5b58\u53d6\u6b0a\u6756\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002",
+ "description": "\u5b58\u53d6\u6b0a\u9650\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002",
"title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408"
},
"user": {
diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py
index 3fdd2e55b0d..fd707f9dd96 100644
--- a/homeassistant/components/skybeacon/sensor.py
+++ b/homeassistant/components/skybeacon/sensor.py
@@ -64,6 +64,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class SkybeaconHumid(SensorEntity):
"""Representation of a Skybeacon humidity sensor."""
+ _attr_unit_of_measurement = PERCENTAGE
+
def __init__(self, name, mon):
"""Initialize a sensor."""
self.mon = mon
@@ -79,11 +81,6 @@ class SkybeaconHumid(SensorEntity):
"""Return the state of the device."""
return self.mon.data["humid"]
- @property
- def unit_of_measurement(self):
- """Return the unit the value is expressed in."""
- return PERCENTAGE
-
@property
def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
@@ -93,6 +90,8 @@ class SkybeaconHumid(SensorEntity):
class SkybeaconTemp(SensorEntity):
"""Representation of a Skybeacon temperature sensor."""
+ _attr_unit_of_measurement = TEMP_CELSIUS
+
def __init__(self, name, mon):
"""Initialize a sensor."""
self.mon = mon
@@ -108,11 +107,6 @@ class SkybeaconTemp(SensorEntity):
"""Return the state of the device."""
return self.mon.data["temp"]
- @property
- def unit_of_measurement(self):
- """Return the unit the value is expressed in."""
- return TEMP_CELSIUS
-
@property
def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py
index ef948440a17..2eb0e6760ed 100644
--- a/homeassistant/components/sma/__init__.py
+++ b/homeassistant/components/sma/__init__.py
@@ -32,6 +32,7 @@ from .const import (
DOMAIN,
PLATFORMS,
PYSMA_COORDINATOR,
+ PYSMA_DEVICE_INFO,
PYSMA_OBJECT,
PYSMA_REMOVE_LISTENER,
PYSMA_SENSORS,
@@ -40,7 +41,9 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
-def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> list[str]:
+def _parse_legacy_options(
+ entry: ConfigEntry, sensor_def: pysma.sensor.Sensors
+) -> list[str]:
"""Parse legacy configuration options.
This will parse the legacy CONF_SENSORS and CONF_CUSTOM configuration options
@@ -50,7 +53,9 @@ def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> list
# Add sensors from the custom config
sensor_def.add(
[
- pysma.Sensor(o[CONF_KEY], n, o[CONF_UNIT], o[CONF_FACTOR], o.get(CONF_PATH))
+ pysma.sensor.Sensor(
+ o[CONF_KEY], n, o[CONF_UNIT], o[CONF_FACTOR], o.get(CONF_PATH)
+ )
for n, o in entry.data.get(CONF_CUSTOM).items()
]
)
@@ -74,9 +79,9 @@ def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> list
# Find and replace sensors removed from pysma
# This only alters the config, the actual sensor migration takes place in _migrate_old_unique_ids
for sensor in config_sensors.copy():
- if sensor in pysma.LEGACY_MAP:
+ if sensor in pysma.const.LEGACY_MAP:
config_sensors.remove(sensor)
- config_sensors.append(pysma.LEGACY_MAP[sensor]["new_sensor"])
+ config_sensors.append(pysma.const.LEGACY_MAP[sensor]["new_sensor"])
# Only sensors from config should be enabled
for sensor in sensor_def:
@@ -88,7 +93,7 @@ def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> list
def _migrate_old_unique_ids(
hass: HomeAssistant,
entry: ConfigEntry,
- sensor_def: pysma.Sensors,
+ sensor_def: pysma.sensor.Sensors,
config_sensors: list[str],
) -> None:
"""Migrate legacy sensor entity_id format to new format."""
@@ -96,16 +101,16 @@ def _migrate_old_unique_ids(
# Create list of all possible sensor names
possible_sensors = set(
- config_sensors + [s.name for s in sensor_def] + list(pysma.LEGACY_MAP)
+ config_sensors + [s.name for s in sensor_def] + list(pysma.const.LEGACY_MAP)
)
for sensor in possible_sensors:
if sensor in sensor_def:
pysma_sensor = sensor_def[sensor]
original_key = pysma_sensor.key
- elif sensor in pysma.LEGACY_MAP:
+ elif sensor in pysma.const.LEGACY_MAP:
# If sensor was removed from pysma we will remap it to the new sensor
- legacy_sensor = pysma.LEGACY_MAP[sensor]
+ legacy_sensor = pysma.const.LEGACY_MAP[sensor]
pysma_sensor = sensor_def[legacy_sensor["new_sensor"]]
original_key = legacy_sensor["old_key"]
else:
@@ -127,13 +132,6 @@ def _migrate_old_unique_ids(
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up sma from a config entry."""
- # Init all default sensors
- sensor_def = pysma.Sensors()
-
- if entry.source == SOURCE_IMPORT:
- config_sensors = _parse_legacy_options(entry, sensor_def)
- _migrate_old_unique_ids(hass, entry, sensor_def, config_sensors)
-
# Init the SMA interface
protocol = "https" if entry.data[CONF_SSL] else "http"
url = f"{protocol}://{entry.data[CONF_HOST]}"
@@ -144,12 +142,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
session = async_get_clientsession(hass, verify_ssl=verify_ssl)
sma = pysma.SMA(session, url, password, group)
+ try:
+ # Get updated device info
+ device_info = await sma.device_info()
+ # Get all device sensors
+ sensor_def = await sma.get_sensors()
+ except (
+ pysma.exceptions.SmaReadException,
+ pysma.exceptions.SmaConnectionException,
+ ) as exc:
+ raise ConfigEntryNotReady from exc
+
+ # Parse legacy options if initial setup was done from yaml
+ if entry.source == SOURCE_IMPORT:
+ config_sensors = _parse_legacy_options(entry, sensor_def)
+ _migrate_old_unique_ids(hass, entry, sensor_def, config_sensors)
+
# Define the coordinator
async def async_update_data():
"""Update the used SMA sensors."""
- values = await sma.read(sensor_def)
- if not values:
- raise UpdateFailed
+ try:
+ await sma.read(sensor_def)
+ except (
+ pysma.exceptions.SmaReadException,
+ pysma.exceptions.SmaConnectionException,
+ ) as exc:
+ raise UpdateFailed(exc) from exc
interval = timedelta(
seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
@@ -184,6 +202,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
PYSMA_COORDINATOR: coordinator,
PYSMA_SENSORS: sensor_def,
PYSMA_REMOVE_LISTENER: remove_stop_listener,
+ PYSMA_DEVICE_INFO: device_info,
}
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py
index a5147098c9f..b95e4e4fe06 100644
--- a/homeassistant/components/sma/config_flow.py
+++ b/homeassistant/components/sma/config_flow.py
@@ -4,11 +4,10 @@ from __future__ import annotations
import logging
from typing import Any
-import aiohttp
import pysma
import voluptuous as vol
-from homeassistant import config_entries, core, exceptions
+from homeassistant import config_entries, core
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -20,7 +19,7 @@ from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from .const import CONF_CUSTOM, CONF_GROUP, DEVICE_INFO, DOMAIN, GROUPS
+from .const import CONF_CUSTOM, CONF_GROUP, DOMAIN, GROUPS
_LOGGER = logging.getLogger(__name__)
@@ -36,15 +35,11 @@ async def validate_input(
sma = pysma.SMA(session, url, data[CONF_PASSWORD], group=data[CONF_GROUP])
- if await sma.new_session() is False:
- raise InvalidAuth
-
+ # new_session raises SmaAuthenticationException on failure
+ await sma.new_session()
device_info = await sma.device_info()
await sma.close_session()
- if not device_info:
- raise CannotRetrieveDeviceInfo
-
return device_info
@@ -63,7 +58,6 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_PASSWORD: vol.UNDEFINED,
CONF_SENSORS: [],
CONF_CUSTOM: {},
- DEVICE_INFO: {},
}
async def async_step_user(
@@ -79,19 +73,19 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._data[CONF_PASSWORD] = user_input[CONF_PASSWORD]
try:
- self._data[DEVICE_INFO] = await validate_input(self.hass, user_input)
- except aiohttp.ClientError:
+ device_info = await validate_input(self.hass, user_input)
+ except pysma.exceptions.SmaConnectionException:
errors["base"] = "cannot_connect"
- except InvalidAuth:
+ except pysma.exceptions.SmaAuthenticationException:
errors["base"] = "invalid_auth"
- except CannotRetrieveDeviceInfo:
+ except pysma.exceptions.SmaReadException:
errors["base"] = "cannot_retrieve_device_info"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if not errors:
- await self.async_set_unique_id(self._data[DEVICE_INFO]["serial"])
+ await self.async_set_unique_id(device_info["serial"])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=self._data[CONF_HOST], data=self._data
@@ -120,7 +114,6 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Import a config flow from configuration."""
device_info = await validate_input(self.hass, import_config)
- import_config[DEVICE_INFO] = device_info
# If unique is configured import was already run
# This means remap was already done, so we can abort
@@ -130,11 +123,3 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(
title=import_config[CONF_HOST], data=import_config
)
-
-
-class InvalidAuth(exceptions.HomeAssistantError):
- """Error to indicate there is invalid auth."""
-
-
-class CannotRetrieveDeviceInfo(exceptions.HomeAssistantError):
- """Error to indicate we cannot retrieve the device information."""
diff --git a/homeassistant/components/sma/const.py b/homeassistant/components/sma/const.py
index 2e1086e48a2..91173d95493 100644
--- a/homeassistant/components/sma/const.py
+++ b/homeassistant/components/sma/const.py
@@ -6,6 +6,7 @@ PYSMA_COORDINATOR = "coordinator"
PYSMA_OBJECT = "pysma"
PYSMA_REMOVE_LISTENER = "remove_listener"
PYSMA_SENSORS = "pysma_sensors"
+PYSMA_DEVICE_INFO = "device_info"
PLATFORMS = ["sensor"]
@@ -14,7 +15,6 @@ CONF_FACTOR = "factor"
CONF_GROUP = "group"
CONF_KEY = "key"
CONF_UNIT = "unit"
-DEVICE_INFO = "device_info"
DEFAULT_SCAN_INTERVAL = 5
diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json
index 8add6f830e8..a48b9ba74ce 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.4.3"],
+ "requirements": ["pysma==0.6.2"],
"codeowners": ["@kellerza", "@rklomp"],
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py
index 04bfb7644a3..3894f864ffb 100644
--- a/homeassistant/components/sma/sensor.py
+++ b/homeassistant/components/sma/sensor.py
@@ -33,10 +33,10 @@ from .const import (
CONF_GROUP,
CONF_KEY,
CONF_UNIT,
- DEVICE_INFO,
DOMAIN,
GROUPS,
PYSMA_COORDINATOR,
+ PYSMA_DEVICE_INFO,
PYSMA_SENSORS,
)
@@ -46,8 +46,8 @@ _LOGGER = logging.getLogger(__name__)
def _check_sensor_schema(conf: dict[str, Any]) -> dict[str, Any]:
"""Check sensors and attributes are valid."""
try:
- valid = [s.name for s in pysma.Sensors()]
- valid += pysma.LEGACY_MAP.keys()
+ valid = [s.name for s in pysma.sensor.Sensors()]
+ valid += pysma.const.LEGACY_MAP.keys()
except (ImportError, AttributeError):
return conf
@@ -124,6 +124,7 @@ async def async_setup_entry(
coordinator = sma_data[PYSMA_COORDINATOR]
used_sensors = sma_data[PYSMA_SENSORS]
+ device_info = sma_data[PYSMA_DEVICE_INFO]
entities = []
for sensor in used_sensors:
@@ -131,7 +132,7 @@ async def async_setup_entry(
SMAsensor(
coordinator,
config_entry.unique_id,
- config_entry.data[DEVICE_INFO],
+ device_info,
sensor,
)
)
@@ -147,7 +148,7 @@ class SMAsensor(CoordinatorEntity, SensorEntity):
coordinator: DataUpdateCoordinator,
config_entry_unique_id: str,
device_info: dict[str, Any],
- pysma_sensor: pysma.Sensor,
+ pysma_sensor: pysma.sensor.Sensor,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
@@ -185,11 +186,15 @@ class SMAsensor(CoordinatorEntity, SensorEntity):
@property
def device_info(self) -> DeviceInfo:
"""Return the device information."""
+ if not self._device_info:
+ return None
+
return {
"identifiers": {(DOMAIN, self._config_entry_unique_id)},
"name": self._device_info["name"],
"manufacturer": self._device_info["manufacturer"],
"model": self._device_info["type"],
+ "sw_version": self._device_info["sw_version"],
}
@property
diff --git a/homeassistant/components/sma/translations/he.json b/homeassistant/components/sma/translations/he.json
new file mode 100644
index 00000000000..8ba7834c335
--- /dev/null
+++ b/homeassistant/components/sma/translations/he.json
@@ -0,0 +1,25 @@
+{
+ "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"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "cannot_retrieve_device_info": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4, \u05d0\u05da \u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d4\u05ea\u05e7\u05df",
+ "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": {
+ "group": "\u05e7\u05d1\u05d5\u05e6\u05d4",
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py
index 3386f7340eb..1037d399e64 100644
--- a/homeassistant/components/smappee/__init__.py
+++ b/homeassistant/components/smappee/__init__.py
@@ -71,7 +71,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Smappee from a zeroconf or config entry."""
if CONF_IP_ADDRESS in entry.data:
if helper.is_smappee_genius(entry.data[CONF_SERIALNUMBER]):
diff --git a/homeassistant/components/smappee/translations/de.json b/homeassistant/components/smappee/translations/de.json
index 6491fbf2d15..121b74e9627 100644
--- a/homeassistant/components/smappee/translations/de.json
+++ b/homeassistant/components/smappee/translations/de.json
@@ -9,7 +9,7 @@
"missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
"no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})."
},
- "flow_title": "Smappee: {name}",
+ "flow_title": "{name}",
"step": {
"environment": {
"data": {
diff --git a/homeassistant/components/smappee/translations/he.json b/homeassistant/components/smappee/translations/he.json
new file mode 100644
index 00000000000..08973c08fa6
--- /dev/null
+++ b/homeassistant/components/smappee/translations/he.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+ "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "environment": {
+ "data": {
+ "environment": "\u05e1\u05d1\u05d9\u05d1\u05d4"
+ }
+ },
+ "local": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ },
+ "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d0\u05ea \u05d4\u05d0\u05d9\u05e0\u05d8\u05d2\u05e8\u05e6\u05d9\u05d4 \u05d4\u05de\u05e7\u05d5\u05de\u05d9\u05ea \u05e9\u05dc Smappee"
+ },
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json
index 15bfd4dc5d2..c2535713626 100644
--- a/homeassistant/components/smappee/translations/hu.json
+++ b/homeassistant/components/smappee/translations/hu.json
@@ -7,7 +7,7 @@
"missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
"no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz."
},
- "flow_title": "Smappee: {name}",
+ "flow_title": "{name}",
"step": {
"local": {
"data": {
diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py
index 82a8ccb354b..3e88221851b 100644
--- a/homeassistant/components/smart_meter_texas/__init__.py
+++ b/homeassistant/components/smart_meter_texas/__init__.py
@@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Smart Meter Texas from a config entry."""
username = entry.data[CONF_USERNAME]
diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py
index 13e93fe362b..f63edcce0fc 100644
--- a/homeassistant/components/smart_meter_texas/sensor.py
+++ b/homeassistant/components/smart_meter_texas/sensor.py
@@ -33,6 +33,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity):
"""Representation of an Smart Meter Texas sensor."""
+ _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR
+
def __init__(self, meter: Meter, coordinator: DataUpdateCoordinator) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
@@ -40,11 +42,6 @@ class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity):
self._state = None
self._available = False
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return ENERGY_KILO_WATT_HOUR
-
@property
def name(self):
"""Device Name."""
diff --git a/homeassistant/components/smart_meter_texas/translations/he.json b/homeassistant/components/smart_meter_texas/translations/he.json
new file mode 100644
index 00000000000..c479d8488f2
--- /dev/null
+++ b/homeassistant/components/smart_meter_texas/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"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py
index 3777f35dbc2..ec4d2c9cad6 100644
--- a/homeassistant/components/smarthab/__init__.py
+++ b/homeassistant/components/smarthab/__init__.py
@@ -52,7 +52,7 @@ async def async_setup(hass, config) -> bool:
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up config entry for SmartHab integration."""
# Assign configuration variables
diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py
index 4fc663fc3d8..64e693941b5 100644
--- a/homeassistant/components/smarthab/cover.py
+++ b/homeassistant/components/smarthab/cover.py
@@ -37,6 +37,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class SmartHabCover(CoverEntity):
"""Representation a cover."""
+ _attr_device_class = DEVICE_CLASS_WINDOW
+
def __init__(self, cover):
"""Initialize a SmartHabCover."""
self._cover = cover
@@ -69,11 +71,6 @@ class SmartHabCover(CoverEntity):
"""Return if the cover is closed or not."""
return self._cover.state == 0
- @property
- def device_class(self) -> str:
- """Return the class of this device, from component DEVICE_CLASSES."""
- return DEVICE_CLASS_WINDOW
-
async def async_open_cover(self, **kwargs):
"""Open the cover."""
await self._cover.async_open()
diff --git a/homeassistant/components/smarthab/translations/he.json b/homeassistant/components/smarthab/translations/he.json
new file mode 100644
index 00000000000..c00515506ac
--- /dev/null
+++ b/homeassistant/components/smarthab/translations/he.json
@@ -0,0 +1,16 @@
+{
+ "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/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py
index 00ea0eb681e..231cfa95263 100644
--- a/homeassistant/components/smartthings/__init__.py
+++ b/homeassistant/components/smartthings/__init__.py
@@ -62,7 +62,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType):
return True
-async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle migration of a previous version config entry.
A config entry created under a previous version must go through the
@@ -84,7 +84,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry):
return False
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Initialize config entry which represents an installed SmartApp."""
# For backwards compat
if entry.unique_id is None:
diff --git a/homeassistant/components/smartthings/translations/he.json b/homeassistant/components/smartthings/translations/he.json
index 5cb63393be8..56fd6e0b4fb 100644
--- a/homeassistant/components/smartthings/translations/he.json
+++ b/homeassistant/components/smartthings/translations/he.json
@@ -8,6 +8,16 @@
"webhook_error": "SmartThings \u05dc\u05d0 \u05d4\u05e6\u05dc\u05d9\u05d7 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05e0\u05e7\u05d5\u05d3\u05ea \u05d4\u05e7\u05e6\u05d4 \u05e9\u05d4\u05d5\u05d2\u05d3\u05e8\u05d4 \u05d1- `base_url`. \u05e2\u05d9\u05d9\u05df \u05d1\u05d3\u05e8\u05d9\u05e9\u05d5\u05ea \u05d4\u05e8\u05db\u05d9\u05d1."
},
"step": {
+ "pat": {
+ "data": {
+ "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4"
+ }
+ },
+ "select_location": {
+ "data": {
+ "location_id": "\u05de\u05d9\u05e7\u05d5\u05dd"
+ }
+ },
"user": {
"description": "\u05d4\u05d6\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} ).",
"title": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9 "
diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py
index 7ab343d2015..6ddeccadc74 100644
--- a/homeassistant/components/smarttub/binary_sensor.py
+++ b/homeassistant/components/smarttub/binary_sensor.py
@@ -1,4 +1,6 @@
"""Platform for binary sensor integration."""
+from __future__ import annotations
+
import logging
from smarttub import SpaError, SpaReminder
@@ -27,9 +29,16 @@ ATTR_CREATED_AT = "created_at"
ATTR_UPDATED_AT = "updated_at"
# how many days to snooze the reminder for
-ATTR_SNOOZE_DAYS = "days"
+ATTR_REMINDER_DAYS = "days"
+RESET_REMINDER_SCHEMA = {
+ vol.Required(ATTR_REMINDER_DAYS): vol.All(
+ vol.Coerce(int), vol.Range(min=30, max=365)
+ )
+}
SNOOZE_REMINDER_SCHEMA = {
- vol.Required(ATTR_SNOOZE_DAYS): vol.All(vol.Coerce(int), vol.Range(min=10, max=120))
+ vol.Required(ATTR_REMINDER_DAYS): vol.All(
+ vol.Coerce(int), vol.Range(min=10, max=120)
+ )
}
@@ -56,11 +65,18 @@ async def async_setup_entry(hass, entry, async_add_entities):
SNOOZE_REMINDER_SCHEMA,
"async_snooze",
)
+ platform.async_register_entity_service(
+ "reset_reminder",
+ RESET_REMINDER_SCHEMA,
+ "async_reset",
+ )
class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity):
"""A binary sensor indicating whether the spa is currently online (connected to the cloud)."""
+ _attr_device_class = DEVICE_CLASS_CONNECTIVITY
+
def __init__(self, coordinator, spa):
"""Initialize the entity."""
super().__init__(coordinator, spa, "Online", "online")
@@ -78,15 +94,12 @@ class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity):
"""Return true if the binary sensor is on."""
return self._state is True
- @property
- def device_class(self) -> str:
- """Return the device class for this entity."""
- return DEVICE_CLASS_CONNECTIVITY
-
class SmartTubReminder(SmartTubEntity, BinarySensorEntity):
"""Reminders for maintenance actions."""
+ _attr_device_class = DEVICE_CLASS_PROBLEM
+
def __init__(self, coordinator, spa, reminder):
"""Initialize the entity."""
super().__init__(
@@ -116,18 +129,19 @@ class SmartTubReminder(SmartTubEntity, BinarySensorEntity):
"""Return the state attributes."""
return {
ATTR_REMINDER_SNOOZED: self.reminder.snoozed,
+ ATTR_REMINDER_DAYS: self.reminder.remaining_days,
}
- @property
- def device_class(self) -> str:
- """Return the device class for this entity."""
- return DEVICE_CLASS_PROBLEM
-
async def async_snooze(self, days):
"""Snooze this reminder for the specified number of days."""
await self.reminder.snooze(days)
await self.coordinator.async_request_refresh()
+ async def async_reset(self, days):
+ """Dismiss this reminder, and reset it to the specified number of days."""
+ await self.reminder.reset(days)
+ await self.coordinator.async_request_refresh()
+
class SmartTubError(SmartTubEntity, BinarySensorEntity):
"""Indicates whether an error code is present.
@@ -135,6 +149,8 @@ class SmartTubError(SmartTubEntity, BinarySensorEntity):
There may be 0 or more errors. If there are >0, we show the first one.
"""
+ _attr_device_class = DEVICE_CLASS_PROBLEM
+
def __init__(self, coordinator, spa):
"""Initialize the entity."""
super().__init__(
@@ -144,7 +160,7 @@ class SmartTubError(SmartTubEntity, BinarySensorEntity):
)
@property
- def error(self) -> SpaError:
+ def error(self) -> SpaError | None:
"""Return the underlying SpaError object for this entity."""
errors = self.coordinator.data[self.spa.id][ATTR_ERRORS]
if len(errors) == 0:
@@ -173,8 +189,3 @@ class SmartTubError(SmartTubEntity, BinarySensorEntity):
ATTR_CREATED_AT: error.created_at.isoformat(),
ATTR_UPDATED_AT: error.updated_at.isoformat(),
}
-
- @property
- def device_class(self) -> str:
- """Return the device class for this entity."""
- return DEVICE_CLASS_PROBLEM
diff --git a/homeassistant/components/smarttub/services.yaml b/homeassistant/components/smarttub/services.yaml
index bb5ee66f1d0..d9890dba35a 100644
--- a/homeassistant/components/smarttub/services.yaml
+++ b/homeassistant/components/smarttub/services.yaml
@@ -64,3 +64,22 @@ snooze_reminder:
min: 10
max: 120
unit_of_measurement: days
+
+reset_reminder:
+ name: Reset a reminder
+ description: Reset a reminder, and set the next time it will be triggered.
+ target:
+ entity:
+ integration: smarttub
+ domain: binary_sensor
+ fields:
+ days:
+ name: Days
+ description: The number of days when the next reminder should trigger.
+ required: true
+ example: 180
+ selector:
+ number:
+ min: 30
+ max: 365
+ unit_of_measurement: days
diff --git a/homeassistant/components/smarttub/translations/de.json b/homeassistant/components/smarttub/translations/de.json
index 0e399b9d018..4549360f761 100644
--- a/homeassistant/components/smarttub/translations/de.json
+++ b/homeassistant/components/smarttub/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "already_configured": "Konto ist bereits konfiguriert",
"reauth_successful": "Die erneute Authentifizierung war erfolgreich"
},
"error": {
diff --git a/homeassistant/components/smarttub/translations/he.json b/homeassistant/components/smarttub/translations/he.json
new file mode 100644
index 00000000000..01ef4d534f6
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/he.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "reauth_confirm": {
+ "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"
+ },
+ "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d3\u05d5\u05d0\"\u05dc \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc SmartTub \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8",
+ "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smhi/translations/he.json b/homeassistant/components/smhi/translations/he.json
index 4c49313d977..99eeb837dc3 100644
--- a/homeassistant/components/smhi/translations/he.json
+++ b/homeassistant/components/smhi/translations/he.json
@@ -3,7 +3,9 @@
"step": {
"user": {
"data": {
- "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
+ "name": "\u05e9\u05dd"
}
}
}
diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py
index d28cb51870b..ec99f2a12ae 100644
--- a/homeassistant/components/smhi/weather.py
+++ b/homeassistant/components/smhi/weather.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
import logging
-from typing import Any, Final, TypedDict
+from typing import Final, TypedDict
import aiohttp
import async_timeout
@@ -31,6 +31,7 @@ from homeassistant.components.weather import (
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
+ Forecast,
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
@@ -235,12 +236,12 @@ class SmhiWeather(WeatherEntity):
return "Swedish weather institute (SMHI)"
@property
- def forecast(self) -> list[dict[str, Any]] | None:
+ def forecast(self) -> list[Forecast] | None:
"""Return the forecast."""
if self._forecasts is None or len(self._forecasts) < 2:
return None
- data = []
+ data: list[Forecast] = []
for forecast in self._forecasts[1:]:
condition = next(
diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py
index 55238c5cf39..52ea32c96d1 100644
--- a/homeassistant/components/sms/__init__.py
+++ b/homeassistant/components/sms/__init__.py
@@ -36,7 +36,7 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Configure Gammu state machine."""
device = entry.data[CONF_DEVICE]
diff --git a/homeassistant/components/sms/translations/he.json b/homeassistant/components/sms/translations/he.json
new file mode 100644
index 00000000000..7d80432febf
--- /dev/null
+++ b/homeassistant/components/sms/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "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."
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py
index f7d3415525d..29f0eb777ba 100644
--- a/homeassistant/components/smtp/notify.py
+++ b/homeassistant/components/smtp/notify.py
@@ -12,6 +12,7 @@ import voluptuous as vol
from homeassistant.components.notify import (
ATTR_DATA,
+ ATTR_TARGET,
ATTR_TITLE,
ATTR_TITLE_DEFAULT,
PLATFORM_SCHEMA,
@@ -182,7 +183,11 @@ class MailNotificationService(BaseNotificationService):
msg = _build_text_msg(message)
msg["Subject"] = subject
- msg["To"] = ",".join(self.recipients)
+
+ recipients = kwargs.get(ATTR_TARGET)
+ if not recipients:
+ recipients = self.recipients
+ msg["To"] = recipients if isinstance(recipients, str) else ",".join(recipients)
if self._sender_name:
msg["From"] = f"{self._sender_name} <{self._sender}>"
else:
@@ -191,14 +196,14 @@ class MailNotificationService(BaseNotificationService):
msg["Date"] = email.utils.format_datetime(dt_util.now())
msg["Message-Id"] = email.utils.make_msgid()
- return self._send_email(msg)
+ return self._send_email(msg, recipients)
- def _send_email(self, msg):
+ def _send_email(self, msg, recipients):
"""Send the message."""
mail = self.connect()
for _ in range(self.tries):
try:
- mail.sendmail(self._sender, self.recipients, msg.as_string())
+ mail.sendmail(self._sender, recipients, msg.as_string())
break
except smtplib.SMTPServerDisconnected:
_LOGGER.warning(
diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py
index 258eafff304..81d9bc5aebe 100644
--- a/homeassistant/components/solaredge/const.py
+++ b/homeassistant/components/solaredge/const.py
@@ -2,7 +2,11 @@
from datetime import timedelta
import logging
+from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
from homeassistant.const import ENERGY_WATT_HOUR, PERCENTAGE, POWER_WATT
+from homeassistant.util import dt as dt_util
+
+from .models import SolarEdgeSensor
DOMAIN = "solaredge"
@@ -23,64 +27,153 @@ ENERGY_DETAILS_DELAY = timedelta(minutes=15)
SCAN_INTERVAL = timedelta(minutes=15)
-# Supported overview sensor types:
-# Key: ['json_key', 'name', unit, icon, default]
-SENSOR_TYPES = {
- "lifetime_energy": [
- "lifeTimeData",
- "Lifetime energy",
- ENERGY_WATT_HOUR,
- "mdi:solar-power",
- False,
- ],
- "energy_this_year": [
- "lastYearData",
- "Energy this year",
- ENERGY_WATT_HOUR,
- "mdi:solar-power",
- False,
- ],
- "energy_this_month": [
- "lastMonthData",
- "Energy this month",
- ENERGY_WATT_HOUR,
- "mdi:solar-power",
- False,
- ],
- "energy_today": [
- "lastDayData",
- "Energy today",
- ENERGY_WATT_HOUR,
- "mdi:solar-power",
- False,
- ],
- "current_power": [
- "currentPower",
- "Current Power",
- POWER_WATT,
- "mdi:solar-power",
- True,
- ],
- "site_details": [None, "Site details", None, None, False],
- "meters": ["meters", "Meters", None, None, False],
- "sensors": ["sensors", "Sensors", None, None, False],
- "gateways": ["gateways", "Gateways", None, None, False],
- "batteries": ["batteries", "Batteries", None, None, False],
- "inverters": ["inverters", "Inverters", None, None, False],
- "power_consumption": ["LOAD", "Power Consumption", None, "mdi:flash", False],
- "solar_power": ["PV", "Solar Power", None, "mdi:solar-power", False],
- "grid_power": ["GRID", "Grid Power", None, "mdi:power-plug", False],
- "storage_power": ["STORAGE", "Storage Power", None, "mdi:car-battery", False],
- "purchased_power": ["Purchased", "Imported Power", None, "mdi:flash", False],
- "production_power": ["Production", "Production Power", None, "mdi:flash", False],
- "consumption_power": ["Consumption", "Consumption Power", None, "mdi:flash", False],
- "selfconsumption_power": [
- "SelfConsumption",
- "SelfConsumption Power",
- None,
- "mdi:flash",
- False,
- ],
- "feedin_power": ["FeedIn", "Exported Power", None, "mdi:flash", False],
- "storage_level": ["STORAGE", "Storage Level", PERCENTAGE, None, False],
-}
+# Supported overview sensors
+SENSOR_TYPES = [
+ SolarEdgeSensor(
+ key="lifetime_energy",
+ json_key="lifeTimeData",
+ name="Lifetime energy",
+ icon="mdi:solar-power",
+ last_reset=dt_util.utc_from_timestamp(0),
+ state_class=STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=ENERGY_WATT_HOUR,
+ ),
+ SolarEdgeSensor(
+ key="energy_this_year",
+ json_key="lastYearData",
+ name="Energy this year",
+ entity_registry_enabled_default=False,
+ icon="mdi:solar-power",
+ unit_of_measurement=ENERGY_WATT_HOUR,
+ ),
+ SolarEdgeSensor(
+ key="energy_this_month",
+ json_key="lastMonthData",
+ name="Energy this month",
+ entity_registry_enabled_default=False,
+ icon="mdi:solar-power",
+ unit_of_measurement=ENERGY_WATT_HOUR,
+ ),
+ SolarEdgeSensor(
+ key="energy_today",
+ json_key="lastDayData",
+ name="Energy today",
+ entity_registry_enabled_default=False,
+ icon="mdi:solar-power",
+ unit_of_measurement=ENERGY_WATT_HOUR,
+ ),
+ SolarEdgeSensor(
+ key="current_power",
+ json_key="currentPower",
+ name="Current Power",
+ icon="mdi:solar-power",
+ state_class=STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=POWER_WATT,
+ ),
+ SolarEdgeSensor(
+ key="site_details",
+ name="Site details",
+ entity_registry_enabled_default=False,
+ ),
+ SolarEdgeSensor(
+ key="meters",
+ json_key="meters",
+ name="Meters",
+ entity_registry_enabled_default=False,
+ ),
+ SolarEdgeSensor(
+ key="sensors",
+ json_key="sensors",
+ name="Sensors",
+ entity_registry_enabled_default=False,
+ ),
+ SolarEdgeSensor(
+ key="gateways",
+ json_key="gateways",
+ name="Gateways",
+ entity_registry_enabled_default=False,
+ ),
+ SolarEdgeSensor(
+ key="batteries",
+ json_key="batteries",
+ name="Batteries",
+ entity_registry_enabled_default=False,
+ ),
+ SolarEdgeSensor(
+ key="inverters",
+ json_key="inverters",
+ name="Inverters",
+ entity_registry_enabled_default=False,
+ ),
+ SolarEdgeSensor(
+ key="power_consumption",
+ json_key="LOAD",
+ name="Power Consumption",
+ entity_registry_enabled_default=False,
+ icon="mdi:flash",
+ ),
+ SolarEdgeSensor(
+ key="solar_power",
+ json_key="PV",
+ name="Solar Power",
+ entity_registry_enabled_default=False,
+ icon="mdi:solar-power",
+ ),
+ SolarEdgeSensor(
+ key="grid_power",
+ json_key="GRID",
+ name="Grid Power",
+ entity_registry_enabled_default=False,
+ icon="mdi:power-plug",
+ ),
+ SolarEdgeSensor(
+ key="storage_power",
+ json_key="STORAGE",
+ name="Storage Power",
+ entity_registry_enabled_default=False,
+ icon="mdi:car-battery",
+ ),
+ SolarEdgeSensor(
+ key="purchased_power",
+ json_key="Purchased",
+ name="Imported Power",
+ entity_registry_enabled_default=False,
+ icon="mdi:flash",
+ ),
+ SolarEdgeSensor(
+ key="production_power",
+ json_key="Production",
+ name="Production Power",
+ entity_registry_enabled_default=False,
+ icon="mdi:flash",
+ ),
+ SolarEdgeSensor(
+ key="consumption_power",
+ json_key="Consumption",
+ name="Consumption Power",
+ entity_registry_enabled_default=False,
+ icon="mdi:flash",
+ ),
+ SolarEdgeSensor(
+ key="selfconsumption_power",
+ json_key="SelfConsumption",
+ name="SelfConsumption Power",
+ entity_registry_enabled_default=False,
+ icon="mdi:flash",
+ ),
+ SolarEdgeSensor(
+ key="feedin_power",
+ json_key="FeedIn",
+ name="Exported Power",
+ entity_registry_enabled_default=False,
+ icon="mdi:flash",
+ ),
+ SolarEdgeSensor(
+ key="storage_level",
+ json_key="STORAGE",
+ name="Storage Level",
+ entity_registry_enabled_default=False,
+ state_class=STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=PERCENTAGE,
+ ),
+]
diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py
new file mode 100644
index 00000000000..b2fe27db808
--- /dev/null
+++ b/homeassistant/components/solaredge/coordinator.py
@@ -0,0 +1,280 @@
+"""Provides the data update coordinators for SolarEdge."""
+from __future__ import annotations
+
+from abc import abstractmethod
+from datetime import date, datetime, timedelta
+
+from solaredge import Solaredge
+from stringcase import snakecase
+
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import (
+ DETAILS_UPDATE_DELAY,
+ ENERGY_DETAILS_DELAY,
+ INVENTORY_UPDATE_DELAY,
+ LOGGER,
+ OVERVIEW_UPDATE_DELAY,
+ POWER_FLOW_UPDATE_DELAY,
+)
+
+
+class SolarEdgeDataService:
+ """Get and update the latest data."""
+
+ def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None:
+ """Initialize the data object."""
+ self.api = api
+ self.site_id = site_id
+
+ self.data = {}
+ self.attributes = {}
+
+ self.hass = hass
+ self.coordinator = None
+
+ @callback
+ def async_setup(self) -> None:
+ """Coordinator creation."""
+ self.coordinator = DataUpdateCoordinator(
+ self.hass,
+ LOGGER,
+ name=str(self),
+ update_method=self.async_update_data,
+ update_interval=self.update_interval,
+ )
+
+ @property
+ @abstractmethod
+ def update_interval(self) -> timedelta:
+ """Update interval."""
+
+ @abstractmethod
+ def update(self) -> None:
+ """Update data in executor."""
+
+ async def async_update_data(self) -> None:
+ """Update data."""
+ await self.hass.async_add_executor_job(self.update)
+
+
+class SolarEdgeOverviewDataService(SolarEdgeDataService):
+ """Get and update the latest overview data."""
+
+ @property
+ def update_interval(self) -> timedelta:
+ """Update interval."""
+ return OVERVIEW_UPDATE_DELAY
+
+ def update(self) -> None:
+ """Update the data from the SolarEdge Monitoring API."""
+ try:
+ data = self.api.get_overview(self.site_id)
+ overview = data["overview"]
+ except KeyError as ex:
+ raise UpdateFailed("Missing overview data, skipping update") from ex
+
+ self.data = {}
+
+ for key, value in overview.items():
+ if key in ["lifeTimeData", "lastYearData", "lastMonthData", "lastDayData"]:
+ data = value["energy"]
+ elif key in ["currentPower"]:
+ data = value["power"]
+ else:
+ data = value
+ self.data[key] = data
+
+ LOGGER.debug("Updated SolarEdge overview: %s", self.data)
+
+
+class SolarEdgeDetailsDataService(SolarEdgeDataService):
+ """Get and update the latest details data."""
+
+ def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None:
+ """Initialize the details data service."""
+ super().__init__(hass, api, site_id)
+
+ self.data = None
+
+ @property
+ def update_interval(self) -> timedelta:
+ """Update interval."""
+ return DETAILS_UPDATE_DELAY
+
+ def update(self) -> None:
+ """Update the data from the SolarEdge Monitoring API."""
+
+ try:
+ data = self.api.get_details(self.site_id)
+ details = data["details"]
+ except KeyError as ex:
+ raise UpdateFailed("Missing details data, skipping update") from ex
+
+ self.data = None
+ self.attributes = {}
+
+ for key, value in details.items():
+ key = snakecase(key)
+
+ if key in ["primary_module"]:
+ for module_key, module_value in value.items():
+ self.attributes[snakecase(module_key)] = module_value
+ elif key in [
+ "peak_power",
+ "type",
+ "name",
+ "last_update_time",
+ "installation_date",
+ ]:
+ self.attributes[key] = value
+ elif key == "status":
+ self.data = value
+
+ LOGGER.debug("Updated SolarEdge details: %s, %s", self.data, self.attributes)
+
+
+class SolarEdgeInventoryDataService(SolarEdgeDataService):
+ """Get and update the latest inventory data."""
+
+ @property
+ def update_interval(self) -> timedelta:
+ """Update interval."""
+ return INVENTORY_UPDATE_DELAY
+
+ def update(self) -> None:
+ """Update the data from the SolarEdge Monitoring API."""
+ try:
+ data = self.api.get_inventory(self.site_id)
+ inventory = data["Inventory"]
+ except KeyError as ex:
+ raise UpdateFailed("Missing inventory data, skipping update") from ex
+
+ self.data = {}
+ self.attributes = {}
+
+ for key, value in inventory.items():
+ self.data[key] = len(value)
+ self.attributes[key] = {key: value}
+
+ LOGGER.debug("Updated SolarEdge inventory: %s, %s", self.data, self.attributes)
+
+
+class SolarEdgeEnergyDetailsService(SolarEdgeDataService):
+ """Get and update the latest power flow data."""
+
+ def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None:
+ """Initialize the power flow data service."""
+ super().__init__(hass, api, site_id)
+
+ self.unit = None
+
+ @property
+ def update_interval(self) -> timedelta:
+ """Update interval."""
+ return ENERGY_DETAILS_DELAY
+
+ def update(self) -> None:
+ """Update the data from the SolarEdge Monitoring API."""
+ try:
+ now = datetime.now()
+ today = date.today()
+ midnight = datetime.combine(today, datetime.min.time())
+ data = self.api.get_energy_details(
+ self.site_id,
+ midnight,
+ now.strftime("%Y-%m-%d %H:%M:%S"),
+ meters=None,
+ time_unit="DAY",
+ )
+ energy_details = data["energyDetails"]
+ except KeyError as ex:
+ raise UpdateFailed("Missing power flow data, skipping update") from ex
+
+ if "meters" not in energy_details:
+ LOGGER.debug(
+ "Missing meters in energy details data. Assuming site does not have any"
+ )
+ return
+
+ self.data = {}
+ self.attributes = {}
+ self.unit = energy_details["unit"]
+
+ for meter in energy_details["meters"]:
+ if "type" not in meter or "values" not in meter:
+ continue
+ if meter["type"] not in [
+ "Production",
+ "SelfConsumption",
+ "FeedIn",
+ "Purchased",
+ "Consumption",
+ ]:
+ continue
+ if len(meter["values"][0]) == 2:
+ self.data[meter["type"]] = meter["values"][0]["value"]
+ self.attributes[meter["type"]] = {"date": meter["values"][0]["date"]}
+
+ LOGGER.debug(
+ "Updated SolarEdge energy details: %s, %s", self.data, self.attributes
+ )
+
+
+class SolarEdgePowerFlowDataService(SolarEdgeDataService):
+ """Get and update the latest power flow data."""
+
+ def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None:
+ """Initialize the power flow data service."""
+ super().__init__(hass, api, site_id)
+
+ self.unit = None
+
+ @property
+ def update_interval(self) -> timedelta:
+ """Update interval."""
+ return POWER_FLOW_UPDATE_DELAY
+
+ def update(self) -> None:
+ """Update the data from the SolarEdge Monitoring API."""
+ try:
+ data = self.api.get_current_power_flow(self.site_id)
+ power_flow = data["siteCurrentPowerFlow"]
+ except KeyError as ex:
+ raise UpdateFailed("Missing power flow data, skipping update") from ex
+
+ power_from = []
+ power_to = []
+
+ if "connections" not in power_flow:
+ LOGGER.debug(
+ "Missing connections in power flow data. Assuming site does not have any"
+ )
+ return
+
+ for connection in power_flow["connections"]:
+ power_from.append(connection["from"].lower())
+ power_to.append(connection["to"].lower())
+
+ self.data = {}
+ self.attributes = {}
+ self.unit = power_flow["unit"]
+
+ for key, value in power_flow.items():
+ if key in ["LOAD", "PV", "GRID", "STORAGE"]:
+ self.data[key] = value["currentPower"]
+ self.attributes[key] = {"status": value["status"]}
+
+ if key in ["GRID"]:
+ export = key.lower() in power_to
+ self.data[key] *= -1 if export else 1
+ self.attributes[key]["flow"] = "export" if export else "import"
+
+ if key in ["STORAGE"]:
+ charge = key.lower() in power_to
+ self.data[key] *= -1 if charge else 1
+ self.attributes[key]["flow"] = "charge" if charge else "discharge"
+ self.attributes[key]["soc"] = value["chargeLevel"]
+
+ LOGGER.debug("Updated SolarEdge power flow: %s, %s", self.data, self.attributes)
diff --git a/homeassistant/components/solaredge/models.py b/homeassistant/components/solaredge/models.py
new file mode 100644
index 00000000000..f91db9ee9ff
--- /dev/null
+++ b/homeassistant/components/solaredge/models.py
@@ -0,0 +1,21 @@
+"""Models for the SolarEdge integration."""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from datetime import datetime
+
+
+@dataclass
+class SolarEdgeSensor:
+ """Represents an SolarEdge Sensor."""
+
+ key: str
+ name: str
+
+ json_key: str | None = None
+ device_class: str | None = None
+ entity_registry_enabled_default: bool = True
+ icon: str | None = None
+ last_reset: datetime | None = None
+ state_class: str | None = None
+ unit_of_measurement: str | None = None
diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py
index b19705edbd3..340b6e0c2c9 100644
--- a/homeassistant/components/solaredge/sensor.py
+++ b/homeassistant/components/solaredge/sensor.py
@@ -1,36 +1,27 @@
"""Support for SolarEdge Monitoring API."""
from __future__ import annotations
-from abc import abstractmethod
-from datetime import date, datetime, timedelta
from typing import Any
from solaredge import Solaredge
-from stringcase import snakecase
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.update_coordinator import (
- CoordinatorEntity,
- DataUpdateCoordinator,
- UpdateFailed,
-)
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from .const import (
- CONF_SITE_ID,
- DATA_API_CLIENT,
- DETAILS_UPDATE_DELAY,
- DOMAIN,
- ENERGY_DETAILS_DELAY,
- INVENTORY_UPDATE_DELAY,
- LOGGER,
- OVERVIEW_UPDATE_DELAY,
- POWER_FLOW_UPDATE_DELAY,
- SENSOR_TYPES,
+from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN, SENSOR_TYPES
+from .coordinator import (
+ SolarEdgeDataService,
+ SolarEdgeDetailsDataService,
+ SolarEdgeEnergyDetailsService,
+ SolarEdgeInventoryDataService,
+ SolarEdgeOverviewDataService,
+ SolarEdgePowerFlowDataService,
)
+from .models import SolarEdgeSensor
async def async_setup_entry(
@@ -50,8 +41,8 @@ async def async_setup_entry(
await service.coordinator.async_refresh()
entities = []
- for sensor_key in SENSOR_TYPES:
- sensor = sensor_factory.create_sensor(sensor_key)
+ for sensor_type in SENSOR_TYPES:
+ sensor = sensor_factory.create_sensor(sensor_type)
if sensor is not None:
entities.append(sensor)
async_add_entities(entities)
@@ -108,59 +99,49 @@ class SolarEdgeSensorFactory:
]:
self.services[key] = (SolarEdgeEnergyDetailsSensor, energy)
- def create_sensor(self, sensor_key: str) -> SolarEdgeSensor:
+ def create_sensor(self, sensor_type: SolarEdgeSensor) -> SolarEdgeSensor:
"""Create and return a sensor based on the sensor_key."""
- sensor_class, service = self.services[sensor_key]
+ sensor_class, service = self.services[sensor_type.key]
- return sensor_class(self.platform_name, sensor_key, service)
+ return sensor_class(self.platform_name, sensor_type, service)
-class SolarEdgeSensor(CoordinatorEntity, SensorEntity):
+class SolarEdgeSensorEntity(CoordinatorEntity, SensorEntity):
"""Abstract class for a solaredge sensor."""
def __init__(
- self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService
+ self,
+ platform_name: str,
+ sensor_type: SolarEdgeSensor,
+ data_service: SolarEdgeDataService,
) -> None:
"""Initialize the sensor."""
super().__init__(data_service.coordinator)
self.platform_name = platform_name
- self.sensor_key = sensor_key
+ self.sensor_type = sensor_type
self.data_service = data_service
- @property
- def unit_of_measurement(self) -> str | None:
- """Return the unit of measurement."""
- return SENSOR_TYPES[self.sensor_key][2]
-
- @property
- def name(self) -> str:
- """Return the name."""
- return f"{self.platform_name} ({SENSOR_TYPES[self.sensor_key][1]})"
-
- @property
- def icon(self) -> str | None:
- """Return the sensor icon."""
- return SENSOR_TYPES[self.sensor_key][3]
+ self._attr_device_class = sensor_type.device_class
+ self._attr_entity_registry_enabled_default = (
+ sensor_type.entity_registry_enabled_default
+ )
+ self._attr_icon = sensor_type.icon
+ self._attr_last_reset = sensor_type.last_reset
+ self._attr_name = f"{platform_name} ({sensor_type.name})"
+ self._attr_state_class = sensor_type.state_class
+ self._attr_unit_of_measurement = sensor_type.unit_of_measurement
-class SolarEdgeOverviewSensor(SolarEdgeSensor):
+class SolarEdgeOverviewSensor(SolarEdgeSensorEntity):
"""Representation of an SolarEdge Monitoring API overview sensor."""
- def __init__(
- self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService
- ) -> None:
- """Initialize the overview sensor."""
- super().__init__(platform_name, sensor_key, data_service)
-
- self._json_key = SENSOR_TYPES[self.sensor_key][0]
-
@property
def state(self) -> str | None:
"""Return the state of the sensor."""
- return self.data_service.data.get(self._json_key)
+ return self.data_service.data.get(self.sensor_type.json_key)
-class SolarEdgeDetailsSensor(SolarEdgeSensor):
+class SolarEdgeDetailsSensor(SolarEdgeSensorEntity):
"""Representation of an SolarEdge Monitoring API details sensor."""
@property
@@ -174,363 +155,76 @@ class SolarEdgeDetailsSensor(SolarEdgeSensor):
return self.data_service.data
-class SolarEdgeInventorySensor(SolarEdgeSensor):
+class SolarEdgeInventorySensor(SolarEdgeSensorEntity):
"""Representation of an SolarEdge Monitoring API inventory sensor."""
- def __init__(self, platform_name, sensor_key, data_service):
- """Initialize the inventory sensor."""
- super().__init__(platform_name, sensor_key, data_service)
-
- self._json_key = SENSOR_TYPES[self.sensor_key][0]
-
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
- return self.data_service.attributes.get(self._json_key)
+ return self.data_service.attributes.get(self.sensor_type.json_key)
@property
def state(self) -> str | None:
"""Return the state of the sensor."""
- return self.data_service.data.get(self._json_key)
+ return self.data_service.data.get(self.sensor_type.json_key)
-class SolarEdgeEnergyDetailsSensor(SolarEdgeSensor):
+class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity):
"""Representation of an SolarEdge Monitoring API power flow sensor."""
- def __init__(self, platform_name, sensor_key, data_service):
+ def __init__(self, platform_name, sensor_type, data_service):
"""Initialize the power flow sensor."""
- super().__init__(platform_name, sensor_key, data_service)
+ super().__init__(platform_name, sensor_type, data_service)
- self._json_key = SENSOR_TYPES[self.sensor_key][0]
+ self._attr_unit_of_measurement = data_service.unit
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
- return self.data_service.attributes.get(self._json_key)
+ return self.data_service.attributes.get(self.sensor_type.json_key)
@property
def state(self) -> str | None:
"""Return the state of the sensor."""
- return self.data_service.data.get(self._json_key)
-
- @property
- def unit_of_measurement(self) -> str | None:
- """Return the unit of measurement."""
- return self.data_service.unit
+ return self.data_service.data.get(self.sensor_type.json_key)
-class SolarEdgePowerFlowSensor(SolarEdgeSensor):
+class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity):
"""Representation of an SolarEdge Monitoring API power flow sensor."""
+ _attr_device_class = DEVICE_CLASS_POWER
+
def __init__(
- self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService
+ self,
+ platform_name: str,
+ sensor_type: SolarEdgeSensor,
+ data_service: SolarEdgeDataService,
) -> None:
"""Initialize the power flow sensor."""
- super().__init__(platform_name, sensor_key, data_service)
+ super().__init__(platform_name, sensor_type, data_service)
- self._json_key = SENSOR_TYPES[self.sensor_key][0]
-
- @property
- def device_class(self) -> str:
- """Device Class."""
- return DEVICE_CLASS_POWER
+ self._attr_unit_of_measurement = data_service.unit
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
- return self.data_service.attributes.get(self._json_key)
+ return self.data_service.attributes.get(self.sensor_type.json_key)
@property
def state(self) -> str | None:
"""Return the state of the sensor."""
- return self.data_service.data.get(self._json_key)
-
- @property
- def unit_of_measurement(self) -> str | None:
- """Return the unit of measurement."""
- return self.data_service.unit
+ return self.data_service.data.get(self.sensor_type.json_key)
-class SolarEdgeStorageLevelSensor(SolarEdgeSensor):
+class SolarEdgeStorageLevelSensor(SolarEdgeSensorEntity):
"""Representation of an SolarEdge Monitoring API storage level sensor."""
- def __init__(
- self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService
- ) -> None:
- """Initialize the storage level sensor."""
- super().__init__(platform_name, sensor_key, data_service)
-
- self._json_key = SENSOR_TYPES[self.sensor_key][0]
-
- @property
- def device_class(self) -> str:
- """Return the device_class of the device."""
- return DEVICE_CLASS_BATTERY
+ _attr_device_class = DEVICE_CLASS_BATTERY
@property
def state(self) -> str | None:
"""Return the state of the sensor."""
- attr = self.data_service.attributes.get(self._json_key)
+ attr = self.data_service.attributes.get(self.sensor_type.json_key)
if attr and "soc" in attr:
return attr["soc"]
return None
-
-
-class SolarEdgeDataService:
- """Get and update the latest data."""
-
- def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None:
- """Initialize the data object."""
- self.api = api
- self.site_id = site_id
-
- self.data = {}
- self.attributes = {}
-
- self.hass = hass
- self.coordinator = None
-
- @callback
- def async_setup(self) -> None:
- """Coordinator creation."""
- self.coordinator = DataUpdateCoordinator(
- self.hass,
- LOGGER,
- name=str(self),
- update_method=self.async_update_data,
- update_interval=self.update_interval,
- )
-
- @property
- @abstractmethod
- def update_interval(self) -> timedelta:
- """Update interval."""
-
- @abstractmethod
- def update(self) -> None:
- """Update data in executor."""
-
- async def async_update_data(self) -> None:
- """Update data."""
- await self.hass.async_add_executor_job(self.update)
-
-
-class SolarEdgeOverviewDataService(SolarEdgeDataService):
- """Get and update the latest overview data."""
-
- @property
- def update_interval(self) -> timedelta:
- """Update interval."""
- return OVERVIEW_UPDATE_DELAY
-
- def update(self) -> None:
- """Update the data from the SolarEdge Monitoring API."""
- try:
- data = self.api.get_overview(self.site_id)
- overview = data["overview"]
- except KeyError as ex:
- raise UpdateFailed("Missing overview data, skipping update") from ex
-
- self.data = {}
-
- for key, value in overview.items():
- if key in ["lifeTimeData", "lastYearData", "lastMonthData", "lastDayData"]:
- data = value["energy"]
- elif key in ["currentPower"]:
- data = value["power"]
- else:
- data = value
- self.data[key] = data
-
- LOGGER.debug("Updated SolarEdge overview: %s", self.data)
-
-
-class SolarEdgeDetailsDataService(SolarEdgeDataService):
- """Get and update the latest details data."""
-
- def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None:
- """Initialize the details data service."""
- super().__init__(hass, api, site_id)
-
- self.data = None
-
- @property
- def update_interval(self) -> timedelta:
- """Update interval."""
- return DETAILS_UPDATE_DELAY
-
- def update(self) -> None:
- """Update the data from the SolarEdge Monitoring API."""
-
- try:
- data = self.api.get_details(self.site_id)
- details = data["details"]
- except KeyError as ex:
- raise UpdateFailed("Missing details data, skipping update") from ex
-
- self.data = None
- self.attributes = {}
-
- for key, value in details.items():
- key = snakecase(key)
-
- if key in ["primary_module"]:
- for module_key, module_value in value.items():
- self.attributes[snakecase(module_key)] = module_value
- elif key in [
- "peak_power",
- "type",
- "name",
- "last_update_time",
- "installation_date",
- ]:
- self.attributes[key] = value
- elif key == "status":
- self.data = value
-
- LOGGER.debug("Updated SolarEdge details: %s, %s", self.data, self.attributes)
-
-
-class SolarEdgeInventoryDataService(SolarEdgeDataService):
- """Get and update the latest inventory data."""
-
- @property
- def update_interval(self) -> timedelta:
- """Update interval."""
- return INVENTORY_UPDATE_DELAY
-
- def update(self) -> None:
- """Update the data from the SolarEdge Monitoring API."""
- try:
- data = self.api.get_inventory(self.site_id)
- inventory = data["Inventory"]
- except KeyError as ex:
- raise UpdateFailed("Missing inventory data, skipping update") from ex
-
- self.data = {}
- self.attributes = {}
-
- for key, value in inventory.items():
- self.data[key] = len(value)
- self.attributes[key] = {key: value}
-
- LOGGER.debug("Updated SolarEdge inventory: %s, %s", self.data, self.attributes)
-
-
-class SolarEdgeEnergyDetailsService(SolarEdgeDataService):
- """Get and update the latest power flow data."""
-
- def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None:
- """Initialize the power flow data service."""
- super().__init__(hass, api, site_id)
-
- self.unit = None
-
- @property
- def update_interval(self) -> timedelta:
- """Update interval."""
- return ENERGY_DETAILS_DELAY
-
- def update(self) -> None:
- """Update the data from the SolarEdge Monitoring API."""
- try:
- now = datetime.now()
- today = date.today()
- midnight = datetime.combine(today, datetime.min.time())
- data = self.api.get_energy_details(
- self.site_id,
- midnight,
- now.strftime("%Y-%m-%d %H:%M:%S"),
- meters=None,
- time_unit="DAY",
- )
- energy_details = data["energyDetails"]
- except KeyError as ex:
- raise UpdateFailed("Missing power flow data, skipping update") from ex
-
- if "meters" not in energy_details:
- LOGGER.debug(
- "Missing meters in energy details data. Assuming site does not have any"
- )
- return
-
- self.data = {}
- self.attributes = {}
- self.unit = energy_details["unit"]
-
- for meter in energy_details["meters"]:
- if "type" not in meter or "values" not in meter:
- continue
- if meter["type"] not in [
- "Production",
- "SelfConsumption",
- "FeedIn",
- "Purchased",
- "Consumption",
- ]:
- continue
- if len(meter["values"][0]) == 2:
- self.data[meter["type"]] = meter["values"][0]["value"]
- self.attributes[meter["type"]] = {"date": meter["values"][0]["date"]}
-
- LOGGER.debug(
- "Updated SolarEdge energy details: %s, %s", self.data, self.attributes
- )
-
-
-class SolarEdgePowerFlowDataService(SolarEdgeDataService):
- """Get and update the latest power flow data."""
-
- def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None:
- """Initialize the power flow data service."""
- super().__init__(hass, api, site_id)
-
- self.unit = None
-
- @property
- def update_interval(self) -> timedelta:
- """Update interval."""
- return POWER_FLOW_UPDATE_DELAY
-
- def update(self) -> None:
- """Update the data from the SolarEdge Monitoring API."""
- try:
- data = self.api.get_current_power_flow(self.site_id)
- power_flow = data["siteCurrentPowerFlow"]
- except KeyError as ex:
- raise UpdateFailed("Missing power flow data, skipping update") from ex
-
- power_from = []
- power_to = []
-
- if "connections" not in power_flow:
- LOGGER.debug(
- "Missing connections in power flow data. Assuming site does not have any"
- )
- return
-
- for connection in power_flow["connections"]:
- power_from.append(connection["from"].lower())
- power_to.append(connection["to"].lower())
-
- self.data = {}
- self.attributes = {}
- self.unit = power_flow["unit"]
-
- for key, value in power_flow.items():
- if key in ["LOAD", "PV", "GRID", "STORAGE"]:
- self.data[key] = value["currentPower"]
- self.attributes[key] = {"status": value["status"]}
-
- if key in ["GRID"]:
- export = key.lower() in power_to
- self.data[key] *= -1 if export else 1
- self.attributes[key]["flow"] = "export" if export else "import"
-
- if key in ["STORAGE"]:
- charge = key.lower() in power_to
- self.data[key] *= -1 if charge else 1
- self.attributes[key]["flow"] = "charge" if charge else "discharge"
- self.attributes[key]["soc"] = value["chargeLevel"]
-
- LOGGER.debug("Updated SolarEdge power flow: %s, %s", self.data, self.attributes)
diff --git a/homeassistant/components/solaredge/translations/he.json b/homeassistant/components/solaredge/translations/he.json
new file mode 100644
index 00000000000..e99a0a713b7
--- /dev/null
+++ b/homeassistant/components/solaredge/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": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py
index f48dcfc6267..b3cfebe9abc 100644
--- a/homeassistant/components/solarlog/__init__.py
+++ b/homeassistant/components/solarlog/__init__.py
@@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant
PLATFORMS = ["sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry for solarlog."""
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
diff --git a/homeassistant/components/solarlog/translations/he.json b/homeassistant/components/solarlog/translations/he.json
new file mode 100644
index 00000000000..84abc77ba79
--- /dev/null
+++ b/homeassistant/components/solarlog/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": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py
index e90fba62824..10ebd26bde2 100644
--- a/homeassistant/components/soma/__init__.py
+++ b/homeassistant/components/soma/__init__.py
@@ -45,7 +45,7 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Soma from a config entry."""
hass.data[DOMAIN] = {}
hass.data[DOMAIN][API] = SomaApi(entry.data[HOST], entry.data[PORT])
diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py
index 436a92a1087..4df12c9f8f5 100644
--- a/homeassistant/components/soma/sensor.py
+++ b/homeassistant/components/soma/sensor.py
@@ -29,10 +29,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class SomaSensor(SomaEntity, SensorEntity):
"""Representation of a Soma cover device."""
- @property
- def device_class(self):
- """Return the class of this device, from component DEVICE_CLASSES."""
- return DEVICE_CLASS_BATTERY
+ _attr_device_class = DEVICE_CLASS_BATTERY
+ _attr_unit_of_measurement = PERCENTAGE
@property
def name(self):
@@ -44,11 +42,6 @@ class SomaSensor(SomaEntity, SensorEntity):
"""Return the state of the entity."""
return self.battery_state
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement this sensor expresses itself in."""
- return PERCENTAGE
-
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
"""Update the sensor with the latest data."""
diff --git a/homeassistant/components/soma/translations/he.json b/homeassistant/components/soma/translations/he.json
new file mode 100644
index 00000000000..cbb3cd76a9d
--- /dev/null
+++ b/homeassistant/components/soma/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py
index f159b19c92a..71d7f7f790c 100644
--- a/homeassistant/components/somfy/__init__.py
+++ b/homeassistant/components/somfy/__init__.py
@@ -1,5 +1,4 @@
"""Support for Somfy hubs."""
-from abc import abstractmethod
from datetime import timedelta
import logging
@@ -8,20 +7,16 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_OPTIMISTIC
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
config_entry_oauth2_flow,
config_validation as cv,
device_registry as dr,
)
-from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.update_coordinator import (
- CoordinatorEntity,
- DataUpdateCoordinator,
-)
from . import api, config_flow
-from .const import API, COORDINATOR, DOMAIN
+from .const import COORDINATOR, DOMAIN
+from .coordinator import SomfyDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -69,7 +64,7 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Somfy from a config entry."""
# Backwards compat
if "auth_implementation" not in entry.data:
@@ -84,25 +79,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
)
data = hass.data[DOMAIN]
- data[API] = api.ConfigEntrySomfyApi(hass, entry, implementation)
-
- async def _update_all_devices():
- """Update all the devices."""
- devices = await hass.async_add_executor_job(data[API].get_devices)
- previous_devices = data[COORDINATOR].data
- # Sometimes Somfy returns an empty list.
- if not devices and previous_devices:
- _LOGGER.debug(
- "No devices returned. Assuming the previous ones are still valid"
- )
- return previous_devices
- return {dev.id: dev for dev in devices}
-
- coordinator = DataUpdateCoordinator(
+ coordinator = SomfyDataUpdateCoordinator(
hass,
_LOGGER,
name="somfy device update",
- update_method=_update_all_devices,
+ client=api.ConfigEntrySomfyApi(hass, entry, implementation),
update_interval=SCAN_INTERVAL,
)
data[COORDINATOR] = coordinator
@@ -140,70 +121,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
- hass.data[DOMAIN].pop(API, None)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
-
-
-class SomfyEntity(CoordinatorEntity, Entity):
- """Representation of a generic Somfy device."""
-
- def __init__(self, coordinator, device_id, somfy_api):
- """Initialize the Somfy device."""
- super().__init__(coordinator)
- self._id = device_id
- self.api = somfy_api
-
- @property
- def device(self):
- """Return data for the device id."""
- return self.coordinator.data[self._id]
-
- @property
- def unique_id(self) -> str:
- """Return the unique id base on the id returned by Somfy."""
- return self._id
-
- @property
- def name(self) -> str:
- """Return the name of the device."""
- return self.device.name
-
- @property
- def device_info(self):
- """Return device specific attributes.
-
- Implemented by platform classes.
- """
- return {
- "identifiers": {(DOMAIN, self.unique_id)},
- "name": self.name,
- "model": self.device.type,
- "via_device": (DOMAIN, self.device.parent_id),
- # For the moment, Somfy only returns their own device.
- "manufacturer": "Somfy",
- }
-
- def has_capability(self, capability: str) -> bool:
- """Test if device has a capability."""
- capabilities = self.device.capabilities
- return bool([c for c in capabilities if c.name == capability])
-
- def has_state(self, state: str) -> bool:
- """Test if device has a state."""
- states = self.device.states
- return bool([c for c in states if c.name == state])
-
- @property
- def assumed_state(self) -> bool:
- """Return if the device has an assumed state."""
- return not bool(self.device.states)
-
- @callback
- def _handle_coordinator_update(self):
- """Process an update from the coordinator."""
- self._create_device()
- super()._handle_coordinator_update()
-
- @abstractmethod
- def _create_device(self):
- """Update the device with the latest data."""
diff --git a/homeassistant/components/somfy/climate.py b/homeassistant/components/somfy/climate.py
index 66602aea3e6..0963321100c 100644
--- a/homeassistant/components/somfy/climate.py
+++ b/homeassistant/components/somfy/climate.py
@@ -23,8 +23,8 @@ from homeassistant.components.climate.const import (
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
-from . import SomfyEntity
-from .const import API, COORDINATOR, DOMAIN
+from .const import COORDINATOR, DOMAIN
+from .entity import SomfyEntity
SUPPORTED_CATEGORIES = {Category.HVAC.value}
@@ -49,10 +49,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Somfy climate platform."""
domain_data = hass.data[DOMAIN]
coordinator = domain_data[COORDINATOR]
- api = domain_data[API]
climates = [
- SomfyClimate(coordinator, device_id, api)
+ SomfyClimate(coordinator, device_id)
for device_id, device in coordinator.data.items()
if SUPPORTED_CATEGORIES & set(device.categories)
]
@@ -63,15 +62,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class SomfyClimate(SomfyEntity, ClimateEntity):
"""Representation of a Somfy thermostat device."""
- def __init__(self, coordinator, device_id, api):
+ def __init__(self, coordinator, device_id):
"""Initialize the Somfy device."""
- super().__init__(coordinator, device_id, api)
+ super().__init__(coordinator, device_id)
self._climate = None
self._create_device()
def _create_device(self):
"""Update the device with the latest data."""
- self._climate = Thermostat(self.device, self.api)
+ self._climate = Thermostat(self.device, self.coordinator.client)
@property
def supported_features(self) -> int:
diff --git a/homeassistant/components/somfy/const.py b/homeassistant/components/somfy/const.py
index 128d6eb76bb..6c7c23e3ab3 100644
--- a/homeassistant/components/somfy/const.py
+++ b/homeassistant/components/somfy/const.py
@@ -2,4 +2,3 @@
DOMAIN = "somfy"
COORDINATOR = "coordinator"
-API = "api"
diff --git a/homeassistant/components/somfy/coordinator.py b/homeassistant/components/somfy/coordinator.py
new file mode 100644
index 00000000000..c9633c4fa4d
--- /dev/null
+++ b/homeassistant/components/somfy/coordinator.py
@@ -0,0 +1,71 @@
+"""Helpers to help coordinate updated."""
+from __future__ import annotations
+
+from datetime import timedelta
+import logging
+
+from pymfy.api.error import QuotaViolationException, SetupNotFoundException
+from pymfy.api.model import Device
+from pymfy.api.somfy_api import SomfyApi
+
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+
+class SomfyDataUpdateCoordinator(DataUpdateCoordinator):
+ """Class to manage fetching Somfy data."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ logger: logging.Logger,
+ *,
+ name: str,
+ client: SomfyApi,
+ update_interval: timedelta | None = None,
+ ) -> None:
+ """Initialize global data updater."""
+ super().__init__(
+ hass,
+ logger,
+ name=name,
+ update_interval=update_interval,
+ )
+ self.data = {}
+ self.client = client
+ self.site_device = {}
+ self.last_site_index = -1
+
+ async def _async_update_data(self) -> dict[str, Device]:
+ """Fetch Somfy data.
+
+ Somfy only allow one call per minute to /site. There is one exception: 2 calls are allowed after site retrieval.
+ """
+ if not self.site_device:
+ sites = await self.hass.async_add_executor_job(self.client.get_sites)
+ if not sites:
+ return {}
+ self.site_device = {site.id: [] for site in sites}
+
+ site_id = self._site_id
+ try:
+ devices = await self.hass.async_add_executor_job(
+ self.client.get_devices, site_id
+ )
+ self.site_device[site_id] = devices
+ except SetupNotFoundException:
+ del self.site_device[site_id]
+ return await self._async_update_data()
+ except QuotaViolationException:
+ self.logger.warning("Quota violation")
+
+ return {dev.id: dev for devices in self.site_device.values() for dev in devices}
+
+ @property
+ def _site_id(self):
+ """Return the next site id to retrieve.
+
+ This tweak is required as Somfy does not allow to call the /site entrypoint more than once per minute.
+ """
+ self.last_site_index = (self.last_site_index + 1) % len(self.site_device)
+ return list(self.site_device.keys())[self.last_site_index]
diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py
index d227bc31227..8ed06b3bcd7 100644
--- a/homeassistant/components/somfy/cover.py
+++ b/homeassistant/components/somfy/cover.py
@@ -21,8 +21,8 @@ from homeassistant.components.cover import (
from homeassistant.const import CONF_OPTIMISTIC, STATE_CLOSED, STATE_OPEN
from homeassistant.helpers.restore_state import RestoreEntity
-from . import SomfyEntity
-from .const import API, COORDINATOR, DOMAIN
+from .const import COORDINATOR, DOMAIN
+from .entity import SomfyEntity
BLIND_DEVICE_CATEGORIES = {Category.INTERIOR_BLIND.value, Category.EXTERIOR_BLIND.value}
SHUTTER_DEVICE_CATEGORIES = {Category.EXTERIOR_BLIND.value}
@@ -37,10 +37,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Somfy cover platform."""
domain_data = hass.data[DOMAIN]
coordinator = domain_data[COORDINATOR]
- api = domain_data[API]
covers = [
- SomfyCover(coordinator, device_id, api, domain_data[CONF_OPTIMISTIC])
+ SomfyCover(coordinator, device_id, domain_data[CONF_OPTIMISTIC])
for device_id, device in coordinator.data.items()
if SUPPORTED_CATEGORIES & set(device.categories)
]
@@ -51,9 +50,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity):
"""Representation of a Somfy cover device."""
- def __init__(self, coordinator, device_id, api, optimistic):
+ def __init__(self, coordinator, device_id, optimistic):
"""Initialize the Somfy device."""
- super().__init__(coordinator, device_id, api)
+ super().__init__(coordinator, device_id)
self.categories = set(self.device.categories)
self.optimistic = optimistic
self._closed = None
@@ -64,7 +63,7 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity):
def _create_device(self) -> Blind:
"""Update the device with the latest data."""
- self._cover = Blind(self.device, self.api)
+ self._cover = Blind(self.device, self.coordinator.client)
@property
def supported_features(self) -> int:
diff --git a/homeassistant/components/somfy/entity.py b/homeassistant/components/somfy/entity.py
new file mode 100644
index 00000000000..88ff86e8849
--- /dev/null
+++ b/homeassistant/components/somfy/entity.py
@@ -0,0 +1,73 @@
+"""Entity representing a Somfy device."""
+
+from abc import abstractmethod
+
+from homeassistant.core import callback
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN
+
+
+class SomfyEntity(CoordinatorEntity, Entity):
+ """Representation of a generic Somfy device."""
+
+ def __init__(self, coordinator, device_id):
+ """Initialize the Somfy device."""
+ super().__init__(coordinator)
+ self._id = device_id
+
+ @property
+ def device(self):
+ """Return data for the device id."""
+ return self.coordinator.data[self._id]
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique id base on the id returned by Somfy."""
+ return self._id
+
+ @property
+ def name(self) -> str:
+ """Return the name of the device."""
+ return self.device.name
+
+ @property
+ def device_info(self):
+ """Return device specific attributes.
+
+ Implemented by platform classes.
+ """
+ return {
+ "identifiers": {(DOMAIN, self.unique_id)},
+ "name": self.name,
+ "model": self.device.type,
+ "via_device": (DOMAIN, self.device.parent_id),
+ # For the moment, Somfy only returns their own device.
+ "manufacturer": "Somfy",
+ }
+
+ def has_capability(self, capability: str) -> bool:
+ """Test if device has a capability."""
+ capabilities = self.device.capabilities
+ return bool([c for c in capabilities if c.name == capability])
+
+ def has_state(self, state: str) -> bool:
+ """Test if device has a state."""
+ states = self.device.states
+ return bool([c for c in states if c.name == state])
+
+ @property
+ def assumed_state(self) -> bool:
+ """Return if the device has an assumed state."""
+ return not bool(self.device.states)
+
+ @callback
+ def _handle_coordinator_update(self):
+ """Process an update from the coordinator."""
+ self._create_device()
+ super()._handle_coordinator_update()
+
+ @abstractmethod
+ def _create_device(self):
+ """Update the device with the latest data."""
diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json
index 8dad4abd6cc..1adbab49fb2 100644
--- a/homeassistant/components/somfy/manifest.json
+++ b/homeassistant/components/somfy/manifest.json
@@ -5,7 +5,7 @@
"documentation": "https://www.home-assistant.io/integrations/somfy",
"dependencies": ["http"],
"codeowners": ["@tetienne"],
- "requirements": ["pymfy==0.9.3"],
+ "requirements": ["pymfy==0.11.0"],
"zeroconf": [
{
"type": "_kizbox._tcp.local.",
diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py
index 34283a1271c..1817ba3fd8c 100644
--- a/homeassistant/components/somfy/sensor.py
+++ b/homeassistant/components/somfy/sensor.py
@@ -6,8 +6,8 @@ from pymfy.api.devices.thermostat import Thermostat
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
-from . import SomfyEntity
-from .const import API, COORDINATOR, DOMAIN
+from .const import COORDINATOR, DOMAIN
+from .entity import SomfyEntity
SUPPORTED_CATEGORIES = {Category.HVAC.value}
@@ -16,10 +16,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Somfy sensor platform."""
domain_data = hass.data[DOMAIN]
coordinator = domain_data[COORDINATOR]
- api = domain_data[API]
sensors = [
- SomfyThermostatBatterySensor(coordinator, device_id, api)
+ SomfyThermostatBatterySensor(coordinator, device_id)
for device_id, device in coordinator.data.items()
if SUPPORTED_CATEGORIES & set(device.categories)
]
@@ -30,27 +29,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity):
"""Representation of a Somfy thermostat battery."""
- def __init__(self, coordinator, device_id, api):
+ _attr_device_class = DEVICE_CLASS_BATTERY
+ _attr_unit_of_measurement = PERCENTAGE
+
+ def __init__(self, coordinator, device_id):
"""Initialize the Somfy device."""
- super().__init__(coordinator, device_id, api)
+ super().__init__(coordinator, device_id)
self._climate = None
self._create_device()
def _create_device(self):
"""Update the device with the latest data."""
- self._climate = Thermostat(self.device, self.api)
+ self._climate = Thermostat(self.device, self.coordinator.client)
@property
def state(self) -> int:
"""Return the state of the sensor."""
return self._climate.get_battery()
-
- @property
- def device_class(self) -> str:
- """Return the device class of the sensor."""
- return DEVICE_CLASS_BATTERY
-
- @property
- def unit_of_measurement(self) -> str:
- """Return the unit of measurement of the sensor."""
- return PERCENTAGE
diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py
index 66eef99d6b5..bd0b1dce5d5 100644
--- a/homeassistant/components/somfy/switch.py
+++ b/homeassistant/components/somfy/switch.py
@@ -4,18 +4,17 @@ from pymfy.api.devices.category import Category
from homeassistant.components.switch import SwitchEntity
-from . import SomfyEntity
-from .const import API, COORDINATOR, DOMAIN
+from .const import COORDINATOR, DOMAIN
+from .entity import SomfyEntity
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Somfy switch platform."""
domain_data = hass.data[DOMAIN]
coordinator = domain_data[COORDINATOR]
- api = domain_data[API]
switches = [
- SomfyCameraShutter(coordinator, device_id, api)
+ SomfyCameraShutter(coordinator, device_id)
for device_id, device in coordinator.data.items()
if Category.CAMERA.value in device.categories
]
@@ -26,14 +25,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class SomfyCameraShutter(SomfyEntity, SwitchEntity):
"""Representation of a Somfy Camera Shutter device."""
- def __init__(self, coordinator, device_id, api):
+ def __init__(self, coordinator, device_id):
"""Initialize the Somfy device."""
- super().__init__(coordinator, device_id, api)
+ super().__init__(coordinator, device_id)
self._create_device()
def _create_device(self):
"""Update the device with the latest data."""
- self.shutter = CameraProtect(self.device, self.api)
+ self.shutter = CameraProtect(self.device, self.coordinator.client)
def turn_on(self, **kwargs) -> None:
"""Turn the entity on."""
diff --git a/homeassistant/components/somfy/translations/he.json b/homeassistant/components/somfy/translations/he.json
new file mode 100644
index 00000000000..c68d7f74d85
--- /dev/null
+++ b/homeassistant/components/somfy/translations/he.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+ "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})",
+ "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."
+ },
+ "create_entry": {
+ "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py
index ae6a77a1e2c..5377846a4c1 100644
--- a/homeassistant/components/somfy_mylink/__init__.py
+++ b/homeassistant/components/somfy_mylink/__init__.py
@@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Somfy MyLink from a config entry."""
hass.data.setdefault(DOMAIN, {})
diff --git a/homeassistant/components/somfy_mylink/translations/de.json b/homeassistant/components/somfy_mylink/translations/de.json
index 4382ccd4a0c..d88d1320279 100644
--- a/homeassistant/components/somfy_mylink/translations/de.json
+++ b/homeassistant/components/somfy_mylink/translations/de.json
@@ -8,14 +8,15 @@
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
- "flow_title": "Somfy MyLink {mac} ({ip})",
+ "flow_title": "{mac} ({ip})",
"step": {
"user": {
"data": {
"host": "Host",
"port": "Port",
"system_id": "System-ID"
- }
+ },
+ "description": "Die System-ID kann in der MyLink-App unter Integration durch Auswahl eines beliebigen Nicht-Cloud-Dienstes abgerufen werden."
}
}
},
@@ -25,16 +26,24 @@
},
"step": {
"entity_config": {
+ "data": {
+ "reverse": "Jalousie ist invertiert"
+ },
"description": "Optionen f\u00fcr `{entity_id}` konfigurieren",
"title": "Entit\u00e4t konfigurieren"
},
"init": {
"data": {
- "entity_id": "Konfiguriere eine bestimmte Entit\u00e4t."
+ "default_reverse": "Standardinvertierungsstatus f\u00fcr nicht konfigurierte Abdeckungen",
+ "entity_id": "Konfiguriere eine bestimmte Entit\u00e4t.",
+ "target_id": "Konfigurieren der Optionen f\u00fcr eine Jalousie."
},
"title": "MyLink-Optionen konfigurieren"
},
"target_config": {
+ "data": {
+ "reverse": "Jalousie ist invertiert"
+ },
"description": "Konfiguriere die Optionen f\u00fcr `{target_name}`",
"title": "MyLink-Cover konfigurieren"
}
diff --git a/homeassistant/components/somfy_mylink/translations/he.json b/homeassistant/components/somfy_mylink/translations/he.json
index 9af5985ac45..e218ba687a4 100644
--- a/homeassistant/components/somfy_mylink/translations/he.json
+++ b/homeassistant/components/somfy_mylink/translations/he.json
@@ -1,5 +1,28 @@
{
+ "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",
+ "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": "{mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "system_id": "\u05de\u05d6\u05d4\u05d4 \u05de\u05e2\u05e8\u05db\u05ea"
+ }
+ }
+ }
+ },
"options": {
+ "abort": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
"step": {
"init": {
"title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc MyLink"
diff --git a/homeassistant/components/somfy_mylink/translations/hu.json b/homeassistant/components/somfy_mylink/translations/hu.json
index 08d0db14866..1cb4db9942a 100644
--- a/homeassistant/components/somfy_mylink/translations/hu.json
+++ b/homeassistant/components/somfy_mylink/translations/hu.json
@@ -8,7 +8,7 @@
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "Somfy MyLink {mac} ({ip})",
+ "flow_title": "{mac} ({ip})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/somfy_mylink/translations/nl.json b/homeassistant/components/somfy_mylink/translations/nl.json
index 4ab135993a1..279010485b6 100644
--- a/homeassistant/components/somfy_mylink/translations/nl.json
+++ b/homeassistant/components/somfy_mylink/translations/nl.json
@@ -27,22 +27,22 @@
"step": {
"entity_config": {
"data": {
- "reverse": "Bedekking is omgekeerd"
+ "reverse": "Rolluik is omgekeerd"
},
"description": "Configureer opties voor `{entity_id}`",
"title": "Entiteit configureren"
},
"init": {
"data": {
- "default_reverse": "Standaard omkeerstatus voor niet-geconfigureerde bedekkingen",
+ "default_reverse": "Standaard omkeerstatus voor niet-geconfigureerde rolluiken",
"entity_id": "Configureer een specifieke entiteit.",
- "target_id": "Configureer opties voor een bedekking."
+ "target_id": "Configureer opties voor een rolluik."
},
"title": "Configureer MyLink-opties"
},
"target_config": {
"data": {
- "reverse": "Bedekking is omgekeerd"
+ "reverse": "Rolluik is omgekeerd"
},
"description": "Configureer opties voor ' {target_name} '",
"title": "Configureer MyLink Cover"
diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py
index 730ba857c49..f0969d063c1 100644
--- a/homeassistant/components/sonarr/__init__.py
+++ b/homeassistant/components/sonarr/__init__.py
@@ -8,7 +8,6 @@ from sonarr import Sonarr, SonarrAccessRestricted, SonarrError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
- ATTR_NAME,
CONF_API_KEY,
CONF_HOST,
CONF_PORT,
@@ -18,17 +17,12 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import (
- ATTR_IDENTIFIERS,
- ATTR_MANUFACTURER,
- ATTR_SOFTWARE_VERSION,
CONF_BASE_PATH,
CONF_UPCOMING_DAYS,
CONF_WANTED_MAX_ITEMS,
DATA_SONARR,
- DATA_UNDO_UPDATE_LISTENER,
DEFAULT_UPCOMING_DAYS,
DEFAULT_WANTED_MAX_ITEMS,
DOMAIN,
@@ -71,12 +65,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except SonarrError as err:
raise ConfigEntryNotReady from err
- undo_listener = entry.add_update_listener(_async_update_listener)
+ entry.async_on_unload(entry.add_update_listener(_async_update_listener))
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
DATA_SONARR: sonarr,
- DATA_UNDO_UPDATE_LISTENER: undo_listener,
}
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
@@ -88,8 +81,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
- hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]()
-
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
@@ -99,54 +90,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
-
-
-class SonarrEntity(Entity):
- """Defines a base Sonarr entity."""
-
- def __init__(
- self,
- *,
- sonarr: Sonarr,
- entry_id: str,
- device_id: str,
- name: str,
- icon: str,
- enabled_default: bool = True,
- ) -> None:
- """Initialize the Sonarr entity."""
- self._entry_id = entry_id
- self._device_id = device_id
- self._enabled_default = enabled_default
- self._icon = icon
- self._name = name
- self.sonarr = sonarr
-
- @property
- def name(self) -> str:
- """Return the name of the entity."""
- return self._name
-
- @property
- def icon(self) -> str:
- """Return the mdi icon of the entity."""
- return self._icon
-
- @property
- def entity_registry_enabled_default(self) -> bool:
- """Return if the entity should be enabled when first added to the entity registry."""
- return self._enabled_default
-
- @property
- def device_info(self) -> DeviceInfo | None:
- """Return device information about the application."""
- if self._device_id is None:
- return None
-
- return {
- ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)},
- ATTR_NAME: "Activity Sensor",
- ATTR_MANUFACTURER: "Sonarr",
- ATTR_SOFTWARE_VERSION: self.sonarr.app.info.version,
- "entry_type": "service",
- }
diff --git a/homeassistant/components/sonarr/const.py b/homeassistant/components/sonarr/const.py
index 52079a9416c..45b26166c92 100644
--- a/homeassistant/components/sonarr/const.py
+++ b/homeassistant/components/sonarr/const.py
@@ -17,7 +17,6 @@ CONF_WANTED_MAX_ITEMS = "wanted_max_items"
# Data
DATA_SONARR = "sonarr"
-DATA_UNDO_UPDATE_LISTENER = "undo_update_listener"
# Defaults
DEFAULT_BASE_PATH = "/api"
diff --git a/homeassistant/components/sonarr/entity.py b/homeassistant/components/sonarr/entity.py
new file mode 100644
index 00000000000..3fc74b1ddb5
--- /dev/null
+++ b/homeassistant/components/sonarr/entity.py
@@ -0,0 +1,39 @@
+"""Base Entity for Sonarr."""
+from __future__ import annotations
+
+from sonarr import Sonarr
+
+from homeassistant.const import ATTR_NAME
+from homeassistant.helpers.entity import DeviceInfo, Entity
+
+from .const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_SOFTWARE_VERSION, DOMAIN
+
+
+class SonarrEntity(Entity):
+ """Defines a base Sonarr entity."""
+
+ def __init__(
+ self,
+ *,
+ sonarr: Sonarr,
+ entry_id: str,
+ device_id: str,
+ ) -> None:
+ """Initialize the Sonarr entity."""
+ self._entry_id = entry_id
+ self._device_id = device_id
+ self.sonarr = sonarr
+
+ @property
+ def device_info(self) -> DeviceInfo | None:
+ """Return device information about the application."""
+ if self._device_id is None:
+ return None
+
+ return {
+ ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)},
+ ATTR_NAME: "Activity Sensor",
+ ATTR_MANUFACTURER: "Sonarr",
+ ATTR_SOFTWARE_VERSION: self.sonarr.app.info.version,
+ "entry_type": "service",
+ }
diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py
index 392c026f49b..f1413aca52f 100644
--- a/homeassistant/components/sonarr/sensor.py
+++ b/homeassistant/components/sonarr/sensor.py
@@ -14,8 +14,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.dt as dt_util
-from . import SonarrEntity
from .const import CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, DATA_SONARR, DOMAIN
+from .entity import SonarrEntity
_LOGGER = logging.getLogger(__name__)
@@ -81,35 +81,25 @@ class SonarrSensor(SonarrEntity, SensorEntity):
unit_of_measurement: str | None = None,
) -> None:
"""Initialize Sonarr sensor."""
- self._unit_of_measurement = unit_of_measurement
self._key = key
- self._unique_id = f"{entry_id}_{key}"
+ self._attr_name = name
+ self._attr_icon = icon
+ self._attr_unique_id = f"{entry_id}_{key}"
+ self._attr_unit_of_measurement = unit_of_measurement
+ self._attr_entity_registry_enabled_default = enabled_default
self.last_update_success = False
super().__init__(
sonarr=sonarr,
entry_id=entry_id,
device_id=entry_id,
- name=name,
- icon=icon,
- enabled_default=enabled_default,
)
- @property
- def unique_id(self) -> str:
- """Return the unique ID for this sensor."""
- return self._unique_id
-
@property
def available(self) -> bool:
"""Return sensor availability."""
return self.last_update_success
- @property
- def unit_of_measurement(self) -> str:
- """Return the unit this state is expressed in."""
- return self._unit_of_measurement
-
class SonarrCommandsSensor(SonarrSensor):
"""Defines a Sonarr Commands sensor."""
@@ -186,7 +176,7 @@ class SonarrDiskspaceSensor(SonarrSensor):
attrs[
disk.path
- ] = f"{free:.2f}/{total:.2f}{self._unit_of_measurement} ({usage:.2f}%)"
+ ] = f"{free:.2f}/{total:.2f}{self.unit_of_measurement} ({usage:.2f}%)"
return attrs
diff --git a/homeassistant/components/sonarr/translations/de.json b/homeassistant/components/sonarr/translations/de.json
index b4ceeeb43b7..c7ca7bd692b 100644
--- a/homeassistant/components/sonarr/translations/de.json
+++ b/homeassistant/components/sonarr/translations/de.json
@@ -9,7 +9,7 @@
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung"
},
- "flow_title": "Sonarr: {name}",
+ "flow_title": "{name}",
"step": {
"reauth_confirm": {
"description": "Die Sonarr-Integration muss manuell mit der Sonarr-API, die unter {host} gehostet wird, neu authentifiziert werden",
diff --git a/homeassistant/components/sonarr/translations/he.json b/homeassistant/components/sonarr/translations/he.json
new file mode 100644
index 00000000000..d033613184e
--- /dev/null
+++ b/homeassistant/components/sonarr/translations/he.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "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"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "reauth_confirm": {
+ "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1"
+ },
+ "user": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json
index e3fa0b5ff21..160f9685308 100644
--- a/homeassistant/components/sonarr/translations/hu.json
+++ b/homeassistant/components/sonarr/translations/hu.json
@@ -9,7 +9,7 @@
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
- "flow_title": "Sonarr: {name}",
+ "flow_title": "{name}",
"step": {
"reauth_confirm": {
"title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se"
diff --git a/homeassistant/components/songpal/translations/de.json b/homeassistant/components/songpal/translations/de.json
index 851e0b5f77c..b9b7fae3f28 100644
--- a/homeassistant/components/songpal/translations/de.json
+++ b/homeassistant/components/songpal/translations/de.json
@@ -7,7 +7,7 @@
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
},
- "flow_title": "Sony Songpal {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"init": {
"description": "M\u00f6chten Sie {name} ({host}) einrichten?"
diff --git a/homeassistant/components/songpal/translations/he.json b/homeassistant/components/songpal/translations/he.json
index b18a6bbcd3a..a8c8d1d0294 100644
--- a/homeassistant/components/songpal/translations/he.json
+++ b/homeassistant/components/songpal/translations/he.json
@@ -5,6 +5,17 @@
},
"error": {
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{name} ({host})",
+ "step": {
+ "init": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?"
+ },
+ "user": {
+ "data": {
+ "endpoint": "\u05e0\u05e7\u05d5\u05d3\u05ea \u05e7\u05e6\u05d4"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/songpal/translations/hu.json b/homeassistant/components/songpal/translations/hu.json
index aa55862c0aa..2bce32d0cb8 100644
--- a/homeassistant/components/songpal/translations/hu.json
+++ b/homeassistant/components/songpal/translations/hu.json
@@ -7,7 +7,7 @@
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
- "flow_title": "Sony Songpal {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"init": {
"description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?"
diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py
index 7f772aec6af..218ddaa8e15 100644
--- a/homeassistant/components/sonos/__init__.py
+++ b/homeassistant/components/sonos/__init__.py
@@ -2,19 +2,21 @@
from __future__ import annotations
import asyncio
-from collections import OrderedDict, deque
+from collections import OrderedDict
import datetime
+from enum import Enum
import logging
import socket
+from urllib.parse import urlparse
import pysonos
from pysonos import events_asyncio
-from pysonos.alarms import Alarm
from pysonos.core import SoCo
from pysonos.exceptions import SoCoException
import voluptuous as vol
from homeassistant import config_entries
+from homeassistant.components import ssdp
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -26,14 +28,16 @@ from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
+from .alarms import SonosAlarms
from .const import (
DATA_SONOS,
DISCOVERY_INTERVAL,
DOMAIN,
PLATFORMS,
- SONOS_ALARM_UPDATE,
SONOS_GROUP_UPDATE,
+ SONOS_REBOOTED,
SONOS_SEEN,
+ UPNP_ST,
)
from .favorites import SonosFavorites
from .speaker import SonosSpeaker
@@ -64,19 +68,27 @@ CONFIG_SCHEMA = vol.Schema(
)
+class SoCoCreationSource(Enum):
+ """Represent the creation source of a SoCo instance."""
+
+ CONFIGURED = "configured"
+ DISCOVERED = "discovered"
+ REBOOTED = "rebooted"
+
+
class SonosData:
"""Storage class for platform global data."""
def __init__(self) -> None:
"""Initialize the data."""
- # OrderedDict behavior used by SonosFavorites
+ # OrderedDict behavior used by SonosAlarms and SonosFavorites
self.discovered: OrderedDict[str, SonosSpeaker] = OrderedDict()
self.favorites: dict[str, SonosFavorites] = {}
- self.alarms: dict[str, Alarm] = {}
- self.processed_alarm_events = deque(maxlen=5)
+ self.alarms: dict[str, SonosAlarms] = {}
self.topology_condition = asyncio.Condition()
- self.discovery_thread = None
self.hosts_heartbeat = None
+ self.ssdp_known: set[str] = set()
+ self.boot_counts: dict[str, int] = {}
async def async_setup(hass, config):
@@ -95,92 +107,135 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+async def async_setup_entry( # noqa: C901
+ hass: HomeAssistant, entry: ConfigEntry
+) -> bool:
"""Set up Sonos from a config entry."""
pysonos.config.EVENTS_MODULE = events_asyncio
if DATA_SONOS not in hass.data:
hass.data[DATA_SONOS] = SonosData()
+ data = hass.data[DATA_SONOS]
config = hass.data[DOMAIN].get("media_player", {})
+ hosts = config.get(CONF_HOSTS, [])
+ discovery_lock = asyncio.Lock()
_LOGGER.debug("Reached async_setup_entry, config=%s", config)
advertise_addr = config.get(CONF_ADVERTISE_ADDR)
if advertise_addr:
pysonos.config.EVENT_ADVERTISE_IP = advertise_addr
- def _stop_discovery(event: Event) -> None:
- data = hass.data[DATA_SONOS]
- if data.discovery_thread:
- data.discovery_thread.stop()
- data.discovery_thread = None
+ async def _async_stop_event_listener(event: Event) -> None:
+ await asyncio.gather(
+ *[speaker.async_unsubscribe() for speaker in data.discovered.values()],
+ return_exceptions=True,
+ )
+ if events_asyncio.event_listener:
+ await events_asyncio.event_listener.async_stop()
+
+ def _stop_manual_heartbeat(event: Event) -> None:
if data.hosts_heartbeat:
data.hosts_heartbeat()
data.hosts_heartbeat = None
- def _discovery(now: datetime.datetime | None = None) -> None:
- """Discover players from network or configuration."""
- hosts = config.get(CONF_HOSTS)
+ def _discovered_player(soco: SoCo) -> None:
+ """Handle a (re)discovered player."""
+ try:
+ speaker_info = soco.get_speaker_info(True)
+ _LOGGER.debug("Adding new speaker: %s", speaker_info)
+ speaker = SonosSpeaker(hass, soco, speaker_info)
+ data.discovered[soco.uid] = speaker
+ for coordinator, coord_dict in [
+ (SonosAlarms, data.alarms),
+ (SonosFavorites, data.favorites),
+ ]:
+ if soco.household_id not in coord_dict:
+ new_coordinator = coordinator(hass, soco.household_id)
+ new_coordinator.setup(soco)
+ coord_dict[soco.household_id] = new_coordinator
+ speaker.setup()
+ except (OSError, SoCoException):
+ _LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True)
- def _discovered_player(soco: SoCo) -> None:
- """Handle a (re)discovered player."""
- try:
- _LOGGER.debug("Reached _discovered_player, soco=%s", soco)
-
- data = hass.data[DATA_SONOS]
-
- if soco.uid not in data.discovered:
- speaker_info = soco.get_speaker_info(True)
- _LOGGER.debug("Adding new speaker: %s", speaker_info)
- speaker = SonosSpeaker(hass, soco, speaker_info)
- data.discovered[soco.uid] = speaker
- if soco.household_id not in data.favorites:
- data.favorites[soco.household_id] = SonosFavorites(
- hass, soco.household_id
- )
- data.favorites[soco.household_id].update()
- speaker.setup()
- else:
- dispatcher_send(hass, f"{SONOS_SEEN}-{soco.uid}", soco)
-
- except SoCoException as ex:
- _LOGGER.debug("SoCoException, ex=%s", ex)
-
- if hosts:
- for host in hosts:
- try:
- _LOGGER.debug("Testing %s", host)
- player = pysonos.SoCo(socket.gethostbyname(host))
- if player.is_visible:
- # Make sure that the player is available
- _ = player.volume
-
- _discovered_player(player)
- except (OSError, SoCoException) as ex:
- _LOGGER.debug("Exception %s", ex)
- if now is None:
- _LOGGER.warning("Failed to initialize '%s'", host)
-
- _LOGGER.debug("Tested all hosts")
- hass.data[DATA_SONOS].hosts_heartbeat = hass.helpers.event.call_later(
- DISCOVERY_INTERVAL.total_seconds(), _discovery
+ def _create_soco(ip_address: str, source: SoCoCreationSource) -> SoCo | None:
+ """Create a soco instance and return if successful."""
+ try:
+ soco = pysonos.SoCo(ip_address)
+ # Ensure that the player is available and UID is cached
+ _ = soco.uid
+ _ = soco.volume
+ return soco
+ except (OSError, SoCoException) as ex:
+ _LOGGER.warning(
+ "Failed to connect to %s player '%s': %s", source.value, ip_address, ex
)
- else:
- _LOGGER.debug("Starting discovery thread")
- hass.data[DATA_SONOS].discovery_thread = pysonos.discover_thread(
- _discovered_player,
- interval=DISCOVERY_INTERVAL.total_seconds(),
- interface_addr=config.get(CONF_INTERFACE_ADDR),
+ return None
+
+ def _manual_hosts(now: datetime.datetime | None = None) -> None:
+ """Players from network configuration."""
+ for host in hosts:
+ ip_addr = socket.gethostbyname(host)
+ known_uid = next(
+ (
+ uid
+ for uid, speaker in data.discovered.items()
+ if speaker.soco.ip_address == ip_addr
+ ),
+ None,
)
- hass.data[DATA_SONOS].discovery_thread.name = "Sonos-Discovery"
+
+ if known_uid:
+ dispatcher_send(hass, f"{SONOS_SEEN}-{known_uid}")
+ else:
+ soco = _create_soco(ip_addr, SoCoCreationSource.CONFIGURED)
+ if soco and soco.is_visible:
+ _discovered_player(soco)
+
+ data.hosts_heartbeat = hass.helpers.event.call_later(
+ DISCOVERY_INTERVAL.total_seconds(), _manual_hosts
+ )
@callback
def _async_signal_update_groups(event):
async_dispatcher_send(hass, SONOS_GROUP_UPDATE)
+ def _discovered_ip(ip_address):
+ soco = _create_soco(ip_address, SoCoCreationSource.DISCOVERED)
+ if soco and soco.is_visible:
+ _discovered_player(soco)
+
+ async def _async_create_discovered_player(uid, discovered_ip, boot_seqnum):
+ """Only create one player at a time."""
+ async with discovery_lock:
+ if uid not in data.discovered:
+ await hass.async_add_executor_job(_discovered_ip, discovered_ip)
+ return
+
+ if boot_seqnum and boot_seqnum > data.boot_counts[uid]:
+ data.boot_counts[uid] = boot_seqnum
+ if soco := await hass.async_add_executor_job(
+ _create_soco, discovered_ip, SoCoCreationSource.REBOOTED
+ ):
+ async_dispatcher_send(hass, f"{SONOS_REBOOTED}-{uid}", soco)
+ else:
+ async_dispatcher_send(hass, f"{SONOS_SEEN}-{uid}")
+
@callback
- def _async_signal_update_alarms(event):
- async_dispatcher_send(hass, SONOS_ALARM_UPDATE)
+ def _async_discovered_player(info):
+ uid = info.get(ssdp.ATTR_UPNP_UDN)
+ if uid.startswith("uuid:"):
+ uid = uid[5:]
+ if boot_seqnum := info.get("X-RINCON-BOOTSEQ"):
+ boot_seqnum = int(boot_seqnum)
+ data.boot_counts.setdefault(uid, boot_seqnum)
+ if uid not in data.ssdp_known:
+ _LOGGER.debug("New discovery: %s", info)
+ data.ssdp_known.add(uid)
+ discovered_ip = urlparse(info[ssdp.ATTR_SSDP_LOCATION]).hostname
+ asyncio.create_task(
+ _async_create_discovered_player(uid, discovered_ip, boot_seqnum)
+ )
async def setup_platforms_and_discovery():
await asyncio.gather(
@@ -189,9 +244,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
for platform in PLATFORMS
]
)
- entry.async_on_unload(
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_discovery)
- )
entry.async_on_unload(
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, _async_signal_update_groups
@@ -199,11 +251,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
entry.async_on_unload(
hass.bus.async_listen_once(
- EVENT_HOMEASSISTANT_START, _async_signal_update_alarms
+ EVENT_HOMEASSISTANT_STOP, _async_stop_event_listener
)
)
_LOGGER.debug("Adding discovery job")
- await hass.async_add_executor_job(_discovery)
+ if hosts:
+ entry.async_on_unload(
+ hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP, _stop_manual_heartbeat
+ )
+ )
+ await hass.async_add_executor_job(_manual_hosts)
+ return
+
+ entry.async_on_unload(
+ ssdp.async_register_callback(
+ hass, _async_discovered_player, {"st": UPNP_ST}
+ )
+ )
hass.async_create_task(setup_platforms_and_discovery())
diff --git a/homeassistant/components/sonos/alarms.py b/homeassistant/components/sonos/alarms.py
new file mode 100644
index 00000000000..98e4b752cad
--- /dev/null
+++ b/homeassistant/components/sonos/alarms.py
@@ -0,0 +1,70 @@
+"""Class representing Sonos alarms."""
+from __future__ import annotations
+
+from collections.abc import Iterator
+import logging
+from typing import Any
+
+from pysonos import SoCo
+from pysonos.alarms import Alarm, get_alarms
+from pysonos.exceptions import SoCoException
+
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+
+from .const import DATA_SONOS, SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM
+from .household_coordinator import SonosHouseholdCoordinator
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SonosAlarms(SonosHouseholdCoordinator):
+ """Coordinator class for Sonos alarms."""
+
+ def __init__(self, *args: Any) -> None:
+ """Initialize the data."""
+ super().__init__(*args)
+ self._alarms: dict[str, Alarm] = {}
+
+ def __iter__(self) -> Iterator:
+ """Return an iterator for the known alarms."""
+ alarms = list(self._alarms.values())
+ return iter(alarms)
+
+ def get(self, alarm_id: str) -> Alarm | None:
+ """Get an Alarm instance."""
+ return self._alarms.get(alarm_id)
+
+ async def async_update_entities(self, soco: SoCo) -> bool:
+ """Create and update alarms entities, return success."""
+ try:
+ new_alarms = await self.hass.async_add_executor_job(self.update_cache, soco)
+ except (OSError, SoCoException) as err:
+ _LOGGER.error("Could not refresh alarms using %s: %s", soco, err)
+ return False
+
+ for alarm in new_alarms:
+ speaker = self.hass.data[DATA_SONOS].discovered[alarm.zone.uid]
+ async_dispatcher_send(
+ self.hass, SONOS_CREATE_ALARM, speaker, [alarm.alarm_id]
+ )
+ async_dispatcher_send(self.hass, f"{SONOS_ALARMS_UPDATED}-{self.household_id}")
+ return True
+
+ def update_cache(self, soco: SoCo) -> set[Alarm]:
+ """Populate cache of known alarms.
+
+ Prune deleted alarms and return new alarms.
+ """
+ soco_alarms = get_alarms(soco)
+ new_alarms = set()
+
+ for alarm in soco_alarms:
+ if alarm.alarm_id not in self._alarms:
+ new_alarms.add(alarm)
+ self._alarms[alarm.alarm_id] = alarm
+
+ for alarm_id, alarm in list(self._alarms.items()):
+ if alarm not in soco_alarms:
+ self._alarms.pop(alarm_id)
+
+ return new_alarms
diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py
index 0a70844e6b5..9072f4cab02 100644
--- a/homeassistant/components/sonos/const.py
+++ b/homeassistant/components/sonos/const.py
@@ -22,6 +22,8 @@ from homeassistant.components.media_player.const import (
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+UPNP_ST = "urn:schemas-upnp-org:device:ZonePlayer:1"
+
DOMAIN = "sonos"
DATA_SONOS = "sonos_media_player"
PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN}
@@ -138,9 +140,10 @@ SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player"
SONOS_ENTITY_CREATED = "sonos_entity_created"
SONOS_POLL_UPDATE = "sonos_poll_update"
SONOS_GROUP_UPDATE = "sonos_group_update"
-SONOS_HOUSEHOLD_UPDATED = "sonos_household_updated"
-SONOS_ALARM_UPDATE = "sonos_alarm_update"
+SONOS_ALARMS_UPDATED = "sonos_alarms_updated"
+SONOS_FAVORITES_UPDATED = "sonos_favorites_updated"
SONOS_STATE_UPDATED = "sonos_state_updated"
+SONOS_REBOOTED = "sonos_rebooted"
SONOS_SEEN = "sonos_seen"
SOURCE_LINEIN = "Line-in"
diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py
index 7d4e168c960..a2b0d7c5a64 100644
--- a/homeassistant/components/sonos/entity.py
+++ b/homeassistant/components/sonos/entity.py
@@ -17,7 +17,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import (
DOMAIN,
SONOS_ENTITY_CREATED,
- SONOS_HOUSEHOLD_UPDATED,
+ SONOS_FAVORITES_UPDATED,
SONOS_POLL_UPDATE,
SONOS_STATE_UPDATED,
)
@@ -54,7 +54,7 @@ class SonosEntity(Entity):
self.async_on_remove(
async_dispatcher_connect(
self.hass,
- f"{SONOS_HOUSEHOLD_UPDATED}-{self.soco.household_id}",
+ f"{SONOS_FAVORITES_UPDATED}-{self.soco.household_id}",
self.async_write_ha_state,
)
)
@@ -64,13 +64,14 @@ class SonosEntity(Entity):
async def async_poll(self, now: datetime.datetime) -> None:
"""Poll the entity if subscriptions fail."""
- if self.speaker.is_first_poll:
+ if not self.speaker.subscriptions_failed:
_LOGGER.warning(
"%s cannot reach [%s], falling back to polling, functionality may be limited",
self.speaker.zone_name,
self.speaker.subscription_address,
)
- self.speaker.is_first_poll = False
+ self.speaker.subscriptions_failed = True
+ await self.speaker.async_unsubscribe()
try:
await self.async_update() # pylint: disable=no-member
except (OSError, SoCoException) as ex:
diff --git a/homeassistant/components/sonos/favorites.py b/homeassistant/components/sonos/favorites.py
index 2f5cab23be2..25fc58ebba2 100644
--- a/homeassistant/components/sonos/favorites.py
+++ b/homeassistant/components/sonos/favorites.py
@@ -2,85 +2,52 @@
from __future__ import annotations
from collections.abc import Iterator
-import datetime
import logging
-from typing import Callable
+from typing import Any
+from pysonos import SoCo
from pysonos.data_structures import DidlFavorite
-from pysonos.events_base import Event as SonosEvent
from pysonos.exceptions import SoCoException
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.dispatcher import dispatcher_send
+from homeassistant.helpers.dispatcher import async_dispatcher_send
-from .const import DATA_SONOS, SONOS_HOUSEHOLD_UPDATED
+from .const import SONOS_FAVORITES_UPDATED
+from .household_coordinator import SonosHouseholdCoordinator
_LOGGER = logging.getLogger(__name__)
-class SonosFavorites:
- """Storage class for Sonos favorites."""
+class SonosFavorites(SonosHouseholdCoordinator):
+ """Coordinator class for Sonos favorites."""
- def __init__(self, hass: HomeAssistant, household_id: str) -> None:
+ def __init__(self, *args: Any) -> None:
"""Initialize the data."""
- self.hass = hass
- self.household_id = household_id
+ super().__init__(*args)
self._favorites: list[DidlFavorite] = []
- self._event_version: str | None = None
- self._next_update: Callable | None = None
def __iter__(self) -> Iterator:
"""Return an iterator for the known favorites."""
favorites = self._favorites.copy()
return iter(favorites)
- @callback
- def async_delayed_update(self, event: SonosEvent) -> None:
- """Add a delay when triggered by an event.
+ async def async_update_entities(self, soco: SoCo) -> bool:
+ """Update the cache and update entities."""
+ try:
+ await self.hass.async_add_executor_job(self.update_cache, soco)
+ except (OSError, SoCoException) as err:
+ _LOGGER.warning("Error requesting favorites from %s: %s", soco, err)
+ return False
- Updated favorites are not always immediately available.
+ async_dispatcher_send(
+ self.hass, f"{SONOS_FAVORITES_UPDATED}-{self.household_id}"
+ )
+ return True
- """
- if not (event_id := event.variables.get("favorites_update_id")):
- return
-
- if not self._event_version:
- self._event_version = event_id
- return
-
- if self._event_version == event_id:
- _LOGGER.debug("Favorites haven't changed (event_id: %s)", event_id)
- return
-
- self._event_version = event_id
-
- if self._next_update:
- self._next_update()
-
- self._next_update = self.hass.helpers.event.async_call_later(3, self.update)
-
- def update(self, now: datetime.datetime | None = None) -> None:
+ def update_cache(self, soco: SoCo) -> None:
"""Request new Sonos favorites from a speaker."""
- new_favorites = None
- discovered = self.hass.data[DATA_SONOS].discovered
-
- for uid, speaker in discovered.items():
- try:
- new_favorites = speaker.soco.music_library.get_sonos_favorites()
- except SoCoException as err:
- _LOGGER.warning(
- "Error requesting favorites from %s: %s", speaker.soco, err
- )
- else:
- # Prefer this SoCo instance next update
- discovered.move_to_end(uid, last=False)
- break
-
- if new_favorites is None:
- _LOGGER.error("Could not reach any speakers to update favorites")
- return
-
+ new_favorites = soco.music_library.get_sonos_favorites()
self._favorites = []
+
for fav in new_favorites:
try:
# exclude non-playable favorites with no linked resources
@@ -89,9 +56,9 @@ class SonosFavorites:
except SoCoException as ex:
# Skip unknown types
_LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex)
+
_LOGGER.debug(
"Cached %s favorites for household %s",
len(self._favorites),
self.household_id,
)
- dispatcher_send(self.hass, f"{SONOS_HOUSEHOLD_UPDATED}-{self.household_id}")
diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py
index 6f22d8ab417..ac8cd00d9db 100644
--- a/homeassistant/components/sonos/helpers.py
+++ b/homeassistant/components/sonos/helpers.py
@@ -7,11 +7,13 @@ from typing import Any, Callable
from pysonos.exceptions import SoCoException, SoCoUPnPException
+from homeassistant.exceptions import HomeAssistantError
+
_LOGGER = logging.getLogger(__name__)
def soco_error(errorcodes: list[str] | None = None) -> Callable:
- """Filter out specified UPnP errors from logs and avoid exceptions."""
+ """Filter out specified UPnP errors and raise exceptions for service calls."""
def decorator(funct: Callable) -> Callable:
"""Decorate functions."""
@@ -21,11 +23,15 @@ def soco_error(errorcodes: list[str] | None = None) -> Callable:
"""Wrap for all soco UPnP exception."""
try:
return funct(*args, **kwargs)
- except SoCoUPnPException as err:
- if not errorcodes or err.error_code not in errorcodes:
- _LOGGER.error("Error on %s with %s", funct.__name__, err)
- except SoCoException as err:
- _LOGGER.error("Error on %s with %s", funct.__name__, err)
+ except (OSError, SoCoException, SoCoUPnPException) as err:
+ error_code = getattr(err, "error_code", None)
+ function = funct.__name__
+ if errorcodes and error_code in errorcodes:
+ _LOGGER.debug(
+ "Error code %s ignored in call to %s", error_code, function
+ )
+ return
+ raise HomeAssistantError(f"Error calling {function}: {err}") from err
return wrapper
diff --git a/homeassistant/components/sonos/household_coordinator.py b/homeassistant/components/sonos/household_coordinator.py
new file mode 100644
index 00000000000..d24ab40b3db
--- /dev/null
+++ b/homeassistant/components/sonos/household_coordinator.py
@@ -0,0 +1,74 @@
+"""Class representing a Sonos household storage helper."""
+from __future__ import annotations
+
+from collections import deque
+from collections.abc import Callable, Coroutine
+import logging
+from typing import Any
+
+from pysonos import SoCo
+
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.debounce import Debouncer
+
+from .const import DATA_SONOS
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SonosHouseholdCoordinator:
+ """Base class for Sonos household-level storage."""
+
+ def __init__(self, hass: HomeAssistant, household_id: str) -> None:
+ """Initialize the data."""
+ self.hass = hass
+ self.household_id = household_id
+ self._processed_events = deque(maxlen=5)
+ self.async_poll: Callable[[], Coroutine[None, None, None]] | None = None
+
+ def setup(self, soco: SoCo) -> None:
+ """Set up the SonosAlarm instance."""
+ self.update_cache(soco)
+ self.hass.add_job(self._async_create_polling_debouncer)
+
+ async def _async_create_polling_debouncer(self) -> None:
+ """Create a polling debouncer in async context.
+
+ Used to ensure redundant poll requests from all speakers are coalesced.
+ """
+ self.async_poll = Debouncer(
+ self.hass,
+ _LOGGER,
+ cooldown=3,
+ immediate=False,
+ function=self._async_poll,
+ ).async_call
+
+ async def _async_poll(self) -> None:
+ """Poll any known speaker."""
+ discovered = self.hass.data[DATA_SONOS].discovered
+
+ for uid, speaker in discovered.items():
+ _LOGGER.debug("Updating %s using %s", type(self).__name__, speaker.soco)
+ success = await self.async_update_entities(speaker.soco)
+
+ if success:
+ # Prefer this SoCo instance next update
+ discovered.move_to_end(uid, last=False)
+ break
+
+ @callback
+ def async_handle_event(self, event_id: str, soco: SoCo) -> None:
+ """Create a task to update from an event callback."""
+ if event_id in self._processed_events:
+ return
+ self._processed_events.append(event_id)
+ self.hass.async_create_task(self.async_update_entities(soco))
+
+ async def async_update_entities(self, soco: SoCo) -> bool:
+ """Update the cache and update entities."""
+ raise NotImplementedError()
+
+ def update_cache(self, soco: SoCo) -> Any:
+ """Update the cache of the household-level feature."""
+ raise NotImplementedError()
diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json
index 7bd9efeda16..a3b031ac07b 100644
--- a/homeassistant/components/sonos/manifest.json
+++ b/homeassistant/components/sonos/manifest.json
@@ -3,7 +3,8 @@
"name": "Sonos",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos",
- "requirements": ["pysonos==0.0.49"],
+ "requirements": ["pysonos==0.0.51"],
+ "dependencies": ["ssdp"],
"after_dependencies": ["plex"],
"ssdp": [
{
diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py
index 1e083b69b61..a4cc6e175ec 100644
--- a/homeassistant/components/sonos/media_player.py
+++ b/homeassistant/components/sonos/media_player.py
@@ -13,7 +13,6 @@ from pysonos.core import (
PLAY_MODE_BY_MEANING,
PLAY_MODES,
)
-from pysonos.exceptions import SoCoUPnPException
import voluptuous as vol
from homeassistant.components.media_player import MediaPlayerEntity
@@ -54,6 +53,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.network import is_internal_request
from .const import (
+ DATA_SONOS,
DOMAIN as SONOS_DOMAIN,
MEDIA_TYPES_TO_SONOS,
PLAYABLE_MEDIA_TYPES,
@@ -294,6 +294,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
async def async_update(self) -> None:
"""Retrieve latest state by polling."""
+ await self.hass.data[DATA_SONOS].favorites[
+ self.speaker.household_id
+ ].async_poll()
await self.hass.async_add_executor_job(self._update)
def _update(self) -> None:
@@ -365,6 +368,11 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
"""Channel currently playing."""
return self.media.channel or None
+ @property
+ def media_playlist(self) -> str | None:
+ """Title of playlist currently playing."""
+ return self.media.playlist_name
+
@property # type: ignore[misc]
def media_artist(self) -> str | None:
"""Artist of current playing media, music track only."""
@@ -508,6 +516,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
If media_id is a Plex payload, attempt Plex->Sonos playback.
+ If media_id is a Sonos or Tidal share link, attempt playback
+ using the respective service.
+
If media_type is "playlist", media_id should be a Sonos
Playlist name. Otherwise, media_id should be a URI.
@@ -517,27 +528,21 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
if media_id and media_id.startswith(PLEX_URI_SCHEME):
media_id = media_id[len(PLEX_URI_SCHEME) :]
play_on_sonos(self.hass, media_type, media_id, self.name) # type: ignore[no-untyped-call]
+ return
+
+ share_link = self.speaker.share_link
+ if share_link.is_share_link(media_id):
+ if kwargs.get(ATTR_MEDIA_ENQUEUE):
+ share_link.add_share_link_to_queue(media_id)
+ else:
+ soco.clear_queue()
+ share_link.add_share_link_to_queue(media_id)
+ soco.play_from_queue(0)
elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK):
if kwargs.get(ATTR_MEDIA_ENQUEUE):
- try:
- if soco.is_service_uri(media_id):
- soco.add_service_uri_to_queue(media_id)
- else:
- soco.add_uri_to_queue(media_id)
- except SoCoUPnPException:
- _LOGGER.error(
- 'Error parsing media uri "%s", '
- "please check it's a valid media resource "
- "supported by Sonos",
- media_id,
- )
+ soco.add_uri_to_queue(media_id)
else:
- if soco.is_service_uri(media_id):
- soco.clear_queue()
- soco.add_service_uri_to_queue(media_id)
- soco.play_from_queue(0)
- else:
- soco.play_uri(media_id)
+ soco.play_uri(media_id)
elif media_type == MEDIA_TYPE_PLAYLIST:
if media_id.startswith("S:"):
item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call]
@@ -546,11 +551,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
try:
playlists = soco.get_sonos_playlists()
playlist = next(p for p in playlists if p.title == media_id)
+ except StopIteration:
+ _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id)
+ else:
soco.clear_queue()
soco.add_to_queue(playlist)
soco.play_from_queue(0)
- except StopIteration:
- _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id)
elif media_type in PLAYABLE_MEDIA_TYPES:
item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call]
diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py
index dc610cee38a..ec59d946c09 100644
--- a/homeassistant/components/sonos/speaker.py
+++ b/homeassistant/components/sonos/speaker.py
@@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
-from collections import deque
from collections.abc import Coroutine
import contextlib
import datetime
@@ -12,12 +11,12 @@ from typing import Any, Callable
import urllib.parse
import async_timeout
-from pysonos.alarms import get_alarms
from pysonos.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo
-from pysonos.data_structures import DidlAudioBroadcast
+from pysonos.data_structures import DidlAudioBroadcast, DidlPlaylistContainer
from pysonos.events_base import Event as SonosEvent, SubscriptionBase
from pysonos.exceptions import SoCoException
from pysonos.music_library import MusicLibrary
+from pysonos.plugins.sharelink import ShareLinkPlugin
from pysonos.snapshot import Snapshot
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
@@ -33,6 +32,7 @@ from homeassistant.helpers.dispatcher import (
)
from homeassistant.util import dt as dt_util
+from .alarms import SonosAlarms
from .const import (
BATTERY_SCAN_INTERVAL,
DATA_SONOS,
@@ -40,13 +40,13 @@ from .const import (
PLATFORMS,
SCAN_INTERVAL,
SEEN_EXPIRE_TIME,
- SONOS_ALARM_UPDATE,
SONOS_CREATE_ALARM,
SONOS_CREATE_BATTERY,
SONOS_CREATE_MEDIA_PLAYER,
SONOS_ENTITY_CREATED,
SONOS_GROUP_UPDATE,
SONOS_POLL_UPDATE,
+ SONOS_REBOOTED,
SONOS_SEEN,
SONOS_STATE_PLAYING,
SONOS_STATE_TRANSITIONING,
@@ -110,6 +110,7 @@ class SonosMedia:
self.duration: float | None = None
self.image_url: str | None = None
self.queue_position: int | None = None
+ self.playlist_name: str | None = None
self.source_name: str | None = None
self.title: str | None = None
self.uri: str | None = None
@@ -124,6 +125,7 @@ class SonosMedia:
self.channel = None
self.duration = None
self.image_url = None
+ self.playlist_name = None
self.queue_position = None
self.source_name = None
self.title = None
@@ -146,13 +148,14 @@ class SonosSpeaker:
self.soco = soco
self.household_id: str = soco.household_id
self.media = SonosMedia(soco)
+ self._share_link_plugin: ShareLinkPlugin | None = None
# Synchronization helpers
- self.is_first_poll: bool = True
self._is_ready: bool = False
self._platforms_ready: set[str] = set()
# Subscriptions and events
+ self.subscriptions_failed: bool = False
self._subscriptions: list[SubscriptionBase] = []
self._resubscription_lock: asyncio.Lock | None = None
self._event_dispatchers: dict[str, Callable] = {}
@@ -164,6 +167,7 @@ class SonosSpeaker:
# Dispatcher handles
self._entity_creation_dispatcher: Callable | None = None
self._group_dispatcher: Callable | None = None
+ self._reboot_dispatcher: Callable | None = None
self._seen_dispatcher: Callable | None = None
# Device information
@@ -207,6 +211,9 @@ class SonosSpeaker:
self._seen_dispatcher = dispatcher_connect(
self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen
)
+ self._reboot_dispatcher = dispatcher_connect(
+ self.hass, f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted
+ )
if battery_info := fetch_battery_info_or_none(self.soco):
self.battery_info = battery_info
@@ -218,7 +225,9 @@ class SonosSpeaker:
else:
self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN})
- if new_alarms := self.update_alarms_for_speaker():
+ if new_alarms := [
+ alarm.alarm_id for alarm in self.alarms if alarm.zone.uid == self.soco.uid
+ ]:
dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms)
else:
self._platforms_ready.add(SWITCH_DOMAIN)
@@ -226,7 +235,7 @@ class SonosSpeaker:
self._event_dispatchers = {
"AlarmClock": self.async_dispatch_alarms,
"AVTransport": self.async_dispatch_media_update,
- "ContentDirectory": self.favorites.async_delayed_update,
+ "ContentDirectory": self.async_dispatch_favorites,
"DeviceProperties": self.async_dispatch_device_properties,
"RenderingControl": self.async_update_volume,
"ZoneGroupTopology": self.async_update_groups,
@@ -239,8 +248,11 @@ class SonosSpeaker:
#
async def async_handle_new_entity(self, entity_type: str) -> None:
"""Listen to new entities to trigger first subscription."""
+ if self._platforms_ready == PLATFORMS:
+ return
+
self._platforms_ready.add(entity_type)
- if self._platforms_ready == PLATFORMS and not self._subscriptions:
+ if self._platforms_ready == PLATFORMS:
self._resubscription_lock = asyncio.Lock()
await self.async_subscribe()
self._is_ready = True
@@ -267,6 +279,11 @@ class SonosSpeaker:
"""Return whether this speaker is available."""
return self._seen_timer is not None
+ @property
+ def alarms(self) -> SonosAlarms:
+ """Return the SonosAlarms instance for this household."""
+ return self.hass.data[DATA_SONOS].alarms[self.household_id]
+
@property
def favorites(self) -> SonosFavorites:
"""Return the SonosFavorites instance for this household."""
@@ -278,9 +295,11 @@ class SonosSpeaker:
return self.coordinator is None
@property
- def processed_alarm_events(self) -> deque[str]:
- """Return the container of processed alarm events."""
- return self.hass.data[DATA_SONOS].processed_alarm_events
+ def share_link(self) -> ShareLinkPlugin:
+ """Cache the ShareLinkPlugin instance for this speaker."""
+ if not self._share_link_plugin:
+ self._share_link_plugin = ShareLinkPlugin(self.soco)
+ return self._share_link_plugin
@property
def subscription_address(self) -> str | None:
@@ -326,6 +345,15 @@ class SonosSpeaker:
subscription.auto_renew_fail = self.async_renew_failed
self._subscriptions.append(subscription)
+ async def async_unsubscribe(self) -> None:
+ """Cancel all subscriptions."""
+ _LOGGER.debug("Unsubscribing from events for %s", self.zone_name)
+ await asyncio.gather(
+ *[subscription.unsubscribe() for subscription in self._subscriptions],
+ return_exceptions=True,
+ )
+ self._subscriptions = []
+
@callback
def async_renew_failed(self, exception: Exception) -> None:
"""Handle a failed subscription renewal."""
@@ -365,13 +393,10 @@ class SonosSpeaker:
@callback
def async_dispatch_alarms(self, event: SonosEvent) -> None:
- """Create a task to update alarms from an event."""
- if not (update_id := event.variables.get("alarm_list_version")):
+ """Add the soco instance associated with the event to the callback."""
+ if not (event_id := event.variables.get("alarm_list_version")):
return
- if update_id in self.processed_alarm_events:
- return
- self.processed_alarm_events.append(update_id)
- self.hass.async_add_executor_job(self.update_alarms)
+ self.alarms.async_handle_event(event_id, self.soco)
@callback
def async_dispatch_device_properties(self, event: SonosEvent) -> None:
@@ -393,6 +418,13 @@ class SonosSpeaker:
await self.async_update_battery_info(battery_dict)
self.async_write_entity_states()
+ @callback
+ def async_dispatch_favorites(self, event: SonosEvent) -> None:
+ """Add the soco instance associated with the event to the callback."""
+ if not (event_id := event.variables.get("favorites_update_id")):
+ return
+ self.favorites.async_handle_event(event_id, self.soco)
+
@callback
def async_dispatch_media_update(self, event: SonosEvent) -> None:
"""Update information about currently playing media from an event."""
@@ -448,7 +480,7 @@ class SonosSpeaker:
SCAN_INTERVAL,
)
- if self._is_ready:
+ if self._is_ready and not self.subscriptions_failed:
done = await self.async_subscribe()
if not done:
assert self._seen_timer is not None
@@ -457,9 +489,11 @@ class SonosSpeaker:
self.async_write_entity_states()
- async def async_unseen(self, now: datetime.datetime | None = None) -> None:
+ async def async_unseen(
+ self, now: datetime.datetime | None = None, will_reconnect: bool = False
+ ) -> None:
"""Make this player unavailable when it was not seen recently."""
- self.async_write_entity_states()
+ self._share_link_plugin = None
if self._seen_timer:
self._seen_timer()
@@ -469,41 +503,21 @@ class SonosSpeaker:
self._poll_timer()
self._poll_timer = None
- for subscription in self._subscriptions:
- await subscription.unsubscribe()
+ await self.async_unsubscribe()
- self._subscriptions = []
+ if not will_reconnect:
+ self.hass.data[DATA_SONOS].ssdp_known.remove(self.soco.uid)
+ self.async_write_entity_states()
- #
- # Alarm management
- #
- def update_alarms_for_speaker(self) -> set[str]:
- """Update current alarm instances.
-
- Updates hass.data[DATA_SONOS].alarms and returns a list of all alarms that are new.
- """
- new_alarms = set()
- stored_alarms = self.hass.data[DATA_SONOS].alarms
- updated_alarms = get_alarms(self.soco)
-
- for alarm in updated_alarms:
- if alarm.zone.uid == self.soco.uid and alarm.alarm_id not in list(
- stored_alarms.keys()
- ):
- new_alarms.add(alarm.alarm_id)
- stored_alarms[alarm.alarm_id] = alarm
-
- for alarm_id, alarm in list(stored_alarms.items()):
- if alarm not in updated_alarms:
- stored_alarms.pop(alarm_id)
-
- return new_alarms
-
- def update_alarms(self) -> None:
- """Update alarms from an event."""
- if new_alarms := self.update_alarms_for_speaker():
- dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms)
- dispatcher_send(self.hass, SONOS_ALARM_UPDATE)
+ async def async_rebooted(self, soco: SoCo) -> None:
+ """Handle a detected speaker reboot."""
+ _LOGGER.warning(
+ "%s rebooted or lost network connectivity, reconnecting with %s",
+ self.zone_name,
+ soco,
+ )
+ await self.async_unseen(will_reconnect=True)
+ await self.async_seen(soco)
#
# Battery management
@@ -599,7 +613,7 @@ class SonosSpeaker:
coordinator_uid = self.soco.uid
slave_uids = []
- with contextlib.suppress(SoCoException):
+ with contextlib.suppress(OSError, SoCoException):
if self.soco.group and self.soco.group.coordinator:
coordinator_uid = self.soco.group.coordinator.uid
slave_uids = [
@@ -882,6 +896,9 @@ class SonosSpeaker:
variables["enqueued_transport_uri"] or variables["current_track_uri"]
)
music_source = self.soco.music_source_from_uri(track_uri)
+ if uri_meta_data := variables.get("enqueued_transport_uri_meta_data"):
+ if isinstance(uri_meta_data, DidlPlaylistContainer):
+ self.media.playlist_name = uri_meta_data.title
else:
self.media.play_mode = self.soco.play_mode
music_source = self.soco.music_source
diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py
index 4b24224f6a0..795eded6ec1 100644
--- a/homeassistant/components/sonos/switch.py
+++ b/homeassistant/components/sonos/switch.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import datetime
import logging
-from pysonos.exceptions import SoCoUPnPException
+from pysonos.exceptions import SoCoException, SoCoUPnPException
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
from homeassistant.const import ATTR_TIME
@@ -15,7 +15,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
DATA_SONOS,
DOMAIN as SONOS_DOMAIN,
- SONOS_ALARM_UPDATE,
+ SONOS_ALARMS_UPDATED,
SONOS_CREATE_ALARM,
)
from .entity import SonosEntity
@@ -35,15 +35,12 @@ ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones"
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Sonos from a config entry."""
- configured_alarms = set()
-
- async def _async_create_entity(speaker: SonosSpeaker, new_alarms: set) -> None:
- for alarm_id in new_alarms:
- if alarm_id not in configured_alarms:
- _LOGGER.debug("Creating alarm with id %s", alarm_id)
- entity = SonosAlarmEntity(alarm_id, speaker)
- async_add_entities([entity])
- configured_alarms.add(alarm_id)
+ async def _async_create_entity(speaker: SonosSpeaker, alarm_ids: list[str]) -> None:
+ entities = []
+ for alarm_id in alarm_ids:
+ _LOGGER.debug("Creating alarm %s on %s", alarm_id, speaker.zone_name)
+ entities.append(SonosAlarmEntity(alarm_id, speaker))
+ async_add_entities(entities)
config_entry.async_on_unload(
async_dispatcher_connect(hass, SONOS_CREATE_ALARM, _async_create_entity)
@@ -57,7 +54,8 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
"""Initialize the switch."""
super().__init__(speaker)
- self._alarm_id = alarm_id
+ self.alarm_id = alarm_id
+ self.household_id = speaker.household_id
self.entity_id = ENTITY_ID_FORMAT.format(f"sonos_alarm_{self.alarm_id}")
async def async_added_to_hass(self) -> None:
@@ -66,20 +64,15 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
self.async_on_remove(
async_dispatcher_connect(
self.hass,
- SONOS_ALARM_UPDATE,
- self.async_update,
+ f"{SONOS_ALARMS_UPDATED}-{self.household_id}",
+ self.async_update_state,
)
)
@property
def alarm(self):
"""Return the alarm instance."""
- return self.hass.data[DATA_SONOS].alarms[self.alarm_id]
-
- @property
- def alarm_id(self):
- """Return the ID of the alarm."""
- return self._alarm_id
+ return self.hass.data[DATA_SONOS].alarms[self.household_id].get(self.alarm_id)
@property
def unique_id(self) -> str:
@@ -100,10 +93,14 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
str(self.alarm.start_time)[0:5],
)
+ async def async_update(self) -> None:
+ """Call the central alarm polling method."""
+ await self.hass.data[DATA_SONOS].alarms[self.household_id].async_poll()
+
@callback
def async_check_if_available(self):
"""Check if alarm exists and remove alarm entity if not available."""
- if self.alarm_id in self.hass.data[DATA_SONOS].alarms:
+ if self.alarm:
return True
_LOGGER.debug("%s has been deleted", self.entity_id)
@@ -114,7 +111,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
return False
- async def async_update(self) -> None:
+ async def async_update_state(self) -> None:
"""Poll the device for the current state."""
if not self.async_check_if_available():
return
@@ -170,6 +167,11 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
or bool(recurrence == "WEEKENDS" and int(timestr) not in range(1, 7))
)
+ @property
+ def available(self) -> bool:
+ """Return whether this alarm is available."""
+ return (self.alarm is not None) and self.speaker.available
+
@property
def is_on(self):
"""Return state of Sonos alarm switch."""
@@ -203,5 +205,5 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
_LOGGER.debug("Toggling the state of %s", self.entity_id)
self.alarm.enabled = turn_on
await self.hass.async_add_executor_job(self.alarm.save)
- except SoCoUPnPException as exc:
- _LOGGER.error("Could not update %s: %s", self.entity_id, exc, exc_info=True)
+ except (OSError, SoCoException, SoCoUPnPException) as exc:
+ _LOGGER.error("Could not update %s: %s", self.entity_id, exc)
diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py
index c49a5691cec..e28aa0b2527 100644
--- a/homeassistant/components/speedtestdotnet/sensor.py
+++ b/homeassistant/components/speedtestdotnet/sensor.py
@@ -1,7 +1,14 @@
"""Support for Speedtest.net internet speed testing sensor."""
+from __future__ import annotations
+
+from typing import Any
+
from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.speedtestdotnet import SpeedTestDataCoordinator
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -19,82 +26,64 @@ from .const import (
)
-async def async_setup_entry(hass, config_entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
"""Set up the Speedtestdotnet sensors."""
-
speedtest_coordinator = hass.data[DOMAIN]
-
- entities = []
- for sensor_type in SENSOR_TYPES:
- entities.append(SpeedtestSensor(speedtest_coordinator, sensor_type))
-
- async_add_entities(entities)
+ async_add_entities(
+ SpeedtestSensor(speedtest_coordinator, sensor_type)
+ for sensor_type in SENSOR_TYPES
+ )
class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity):
"""Implementation of a speedtest.net sensor."""
- def __init__(self, coordinator, sensor_type):
+ coordinator: SpeedTestDataCoordinator
+
+ _attr_icon = ICON
+
+ def __init__(self, coordinator: SpeedTestDataCoordinator, sensor_type: str) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
- self._name = SENSOR_TYPES[sensor_type][0]
self.type = sensor_type
- self._unit_of_measurement = SENSOR_TYPES[self.type][1]
- self._state = None
+
+ self._attr_name = f"{DEFAULT_NAME} {SENSOR_TYPES[sensor_type][0]}"
+ self._attr_unit_of_measurement = SENSOR_TYPES[self.type][1]
+ self._attr_unique_id = sensor_type
@property
- def name(self):
- """Return the name of the sensor."""
- return f"{DEFAULT_NAME} {self._name}"
-
- @property
- def unique_id(self):
- """Return sensor unique_id."""
- return self.type
-
- @property
- def state(self):
- """Return the state of the device."""
- return self._state
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return self._unit_of_measurement
-
- @property
- def icon(self):
- """Return icon."""
- return ICON
-
- @property
- def extra_state_attributes(self):
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes."""
if not self.coordinator.data:
return None
+
attributes = {
ATTR_ATTRIBUTION: ATTRIBUTION,
ATTR_SERVER_NAME: self.coordinator.data["server"]["name"],
ATTR_SERVER_COUNTRY: self.coordinator.data["server"]["country"],
ATTR_SERVER_ID: self.coordinator.data["server"]["id"],
}
+
if self.type == "download":
attributes[ATTR_BYTES_RECEIVED] = self.coordinator.data["bytes_received"]
-
- if self.type == "upload":
+ elif self.type == "upload":
attributes[ATTR_BYTES_SENT] = self.coordinator.data["bytes_sent"]
return attributes
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
if state:
- self._state = state.state
+ self._attr_state = state.state
@callback
- def update():
+ def update() -> None:
"""Update state."""
self._update_state()
self.async_write_ha_state()
@@ -102,12 +91,14 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity):
self.async_on_remove(self.coordinator.async_add_listener(update))
self._update_state()
- def _update_state(self):
+ def _update_state(self) -> None:
"""Update sensors state."""
- if self.coordinator.data:
- if self.type == "ping":
- self._state = self.coordinator.data["ping"]
- elif self.type == "download":
- self._state = round(self.coordinator.data["download"] / 10 ** 6, 2)
- elif self.type == "upload":
- self._state = round(self.coordinator.data["upload"] / 10 ** 6, 2)
+ if not self.coordinator.data:
+ return
+
+ if self.type == "ping":
+ self._attr_state = self.coordinator.data["ping"]
+ elif self.type == "download":
+ self._attr_state = round(self.coordinator.data["download"] / 10 ** 6, 2)
+ elif self.type == "upload":
+ self._attr_state = round(self.coordinator.data["upload"] / 10 ** 6, 2)
diff --git a/homeassistant/components/speedtestdotnet/translations/he.json b/homeassistant/components/speedtestdotnet/translations/he.json
new file mode 100644
index 00000000000..08506bf3437
--- /dev/null
+++ b/homeassistant/components/speedtestdotnet/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "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?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spider/translations/he.json b/homeassistant/components/spider/translations/he.json
new file mode 100644
index 00000000000..977aa3a9d5e
--- /dev/null
+++ b/homeassistant/components/spider/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea."
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py
index e9ae2367273..1c92e2ce51a 100644
--- a/homeassistant/components/spotify/media_player.py
+++ b/homeassistant/components/spotify/media_player.py
@@ -67,8 +67,6 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
-ICON = "mdi:spotify"
-
SCAN_INTERVAL = timedelta(seconds=30)
SUPPORT_SPOTIFY = (
@@ -211,12 +209,12 @@ def spotify_exception_handler(func):
def wrapper(self, *args, **kwargs):
try:
result = func(self, *args, **kwargs)
- self.player_available = True
+ self._attr_available = True
return result
except requests.RequestException:
- self.player_available = False
+ self._attr_available = False
except SpotifyException as exc:
- self.player_available = False
+ self._attr_available = False
if exc.reason == "NO_ACTIVE_DEVICE":
raise HomeAssistantError("No active playback device found") from None
@@ -226,6 +224,10 @@ def spotify_exception_handler(func):
class SpotifyMediaPlayer(MediaPlayerEntity):
"""Representation of a Spotify controller."""
+ _attr_icon = "mdi:spotify"
+ _attr_media_content_type = MEDIA_TYPE_MUSIC
+ _attr_media_image_remotely_accessible = False
+
def __init__(
self,
session: OAuth2Session,
@@ -247,40 +249,22 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
self._currently_playing: dict | None = {}
self._devices: list[dict] | None = []
self._playlist: dict | None = None
- self._spotify: Spotify = None
- self.player_available = False
-
- @property
- def name(self) -> str:
- """Return the name."""
- return self._name
-
- @property
- def icon(self) -> str:
- """Return the icon."""
- return ICON
-
- @property
- def available(self) -> bool:
- """Return True if entity is available."""
- return self.player_available
-
- @property
- def unique_id(self) -> str:
- """Return the unique ID."""
- return self._id
+ self._attr_name = self._name
+ self._attr_unique_id = user_id
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this entity."""
+ model = "Spotify Free"
if self._me is not None:
- model = self._me["product"]
+ product = self._me["product"]
+ model = f"Spotify {product}"
return {
"identifiers": {(DOMAIN, self._id)},
"manufacturer": "Spotify AB",
- "model": f"Spotify {model}".rstrip(),
+ "model": model,
"name": self._name,
}
@@ -304,11 +288,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
item = self._currently_playing.get("item") or {}
return item.get("uri")
- @property
- def media_content_type(self) -> str | None:
- """Return the media type."""
- return MEDIA_TYPE_MUSIC
-
@property
def media_duration(self) -> int | None:
"""Duration of current playing media in seconds."""
@@ -340,11 +319,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
return None
return fetch_image_url(self._currently_playing["item"]["album"])
- @property
- def media_image_remotely_accessible(self) -> bool:
- """If the image url is remotely accessible."""
- return False
-
@property
def media_title(self) -> str | None:
"""Return the media title."""
@@ -357,7 +331,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
if self._currently_playing.get("item") is None:
return None
return ", ".join(
- [artist["name"] for artist in self._currently_playing["item"]["artists"]]
+ artist["name"] for artist in self._currently_playing["item"]["artists"]
)
@property
diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json
index 1ed426282fe..f92db780e82 100644
--- a/homeassistant/components/spotify/translations/de.json
+++ b/homeassistant/components/spotify/translations/de.json
@@ -14,7 +14,7 @@
"title": "W\u00e4hle die Authentifizierungsmethode"
},
"reauth_confirm": {
- "description": "Die Spotify-Integration muss sich bei Spotify f\u00fcr das Konto neu authentifizieren: {Konto}",
+ "description": "Die Spotify-Integration muss sich bei Spotify f\u00fcr das Konto neu authentifizieren: {account}",
"title": "Integration erneut authentifizieren"
}
}
diff --git a/homeassistant/components/spotify/translations/he.json b/homeassistant/components/spotify/translations/he.json
new file mode 100644
index 00000000000..a2605ed88f6
--- /dev/null
+++ b/homeassistant/components/spotify/translations/he.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ },
+ "reauth_confirm": {
+ "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json
index e9805040648..a2a197a0eb0 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.13"],
+ "requirements": ["sqlalchemy==1.4.17"],
"codeowners": ["@dgomes"],
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py
index f680c4f5f2f..9bdc8ac9669 100644
--- a/homeassistant/components/squeezebox/__init__.py
+++ b/homeassistant/components/squeezebox/__init__.py
@@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = [MP_DOMAIN]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Logitech Squeezebox from a config entry."""
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
diff --git a/homeassistant/components/squeezebox/translations/de.json b/homeassistant/components/squeezebox/translations/de.json
index c64e1ae3a1c..295aeddfa86 100644
--- a/homeassistant/components/squeezebox/translations/de.json
+++ b/homeassistant/components/squeezebox/translations/de.json
@@ -10,7 +10,7 @@
"no_server_found": "Konnte den Server nicht automatisch entdecken.",
"unknown": "Unerwarteter Fehler"
},
- "flow_title": "Logitech Squeezebox",
+ "flow_title": "{host}",
"step": {
"edit": {
"data": {
diff --git a/homeassistant/components/squeezebox/translations/he.json b/homeassistant/components/squeezebox/translations/he.json
new file mode 100644
index 00000000000..0a0ab6a9b5b
--- /dev/null
+++ b/homeassistant/components/squeezebox/translations/he.json
@@ -0,0 +1,28 @@
+{
+ "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",
+ "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": "{host}",
+ "step": {
+ "edit": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/squeezebox/translations/hu.json b/homeassistant/components/squeezebox/translations/hu.json
index 216badd15c6..e9d7413ebfa 100644
--- a/homeassistant/components/squeezebox/translations/hu.json
+++ b/homeassistant/components/squeezebox/translations/hu.json
@@ -10,7 +10,7 @@
"no_server_found": "Nem siker\u00fclt automatikusan felfedezni a szervert.",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "Logitech Squeezebox: {host}",
+ "flow_title": "{host}",
"step": {
"edit": {
"data": {
diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py
index 785558ba34e..0e25e3f21f6 100644
--- a/homeassistant/components/srp_energy/__init__.py
+++ b/homeassistant/components/srp_energy/__init__.py
@@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the SRP Energy component from a config entry."""
# Store an SrpEnergyClient object for your srp_energy to access
try:
@@ -35,9 +35,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
return True
-async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
# unload srp client
hass.data[SRP_ENERGY_DOMAIN] = None
# Remove config entry
- return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
+ return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/srp_energy/translations/he.json b/homeassistant/components/srp_energy/translations/he.json
new file mode 100644
index 00000000000..b38e5b12c8b
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/he.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea."
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "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": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py
index d9f74e5e776..d03f8967311 100644
--- a/homeassistant/components/ssdp/__init__.py
+++ b/homeassistant/components/ssdp/__init__.py
@@ -4,18 +4,27 @@ from __future__ import annotations
import asyncio
from collections.abc import Mapping
from datetime import timedelta
+from ipaddress import IPv4Address, IPv6Address
import logging
-from typing import Any
+from typing import Any, Callable
-import aiohttp
-from async_upnp_client.search import async_search
-from defusedxml import ElementTree
-from netdisco import ssdp, util
+from async_upnp_client.search import SSDPListener
+from async_upnp_client.utils import CaseInsensitiveDict
-from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
-from homeassistant.core import callback
+from homeassistant import config_entries
+from homeassistant.components import network
+from homeassistant.const import (
+ EVENT_HOMEASSISTANT_STARTED,
+ EVENT_HOMEASSISTANT_STOP,
+ MATCH_ALL,
+)
+from homeassistant.core import CoreState, HomeAssistant, callback as core_callback
from homeassistant.helpers.event import async_track_time_interval
-from homeassistant.loader import async_get_ssdp
+from homeassistant.helpers.typing import ConfigType
+from homeassistant.loader import async_get_ssdp, bind_hass
+
+from .descriptions import DescriptionManager
+from .flow import FlowDispatcher, SSDPFlow
DOMAIN = "ssdp"
SCAN_INTERVAL = timedelta(seconds=60)
@@ -40,188 +49,332 @@ ATTR_UPNP_UDN = "UDN"
ATTR_UPNP_UPC = "UPC"
ATTR_UPNP_PRESENTATION_URL = "presentationURL"
+
+DISCOVERY_MAPPING = {
+ "usn": ATTR_SSDP_USN,
+ "ext": ATTR_SSDP_EXT,
+ "server": ATTR_SSDP_SERVER,
+ "st": ATTR_SSDP_ST,
+ "location": ATTR_SSDP_LOCATION,
+}
+
+
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass, config):
+@bind_hass
+def async_register_callback(
+ hass: HomeAssistant,
+ callback: Callable[[dict], None],
+ match_dict: None | dict[str, str] = None,
+) -> Callable[[], None]:
+ """Register to receive a callback on ssdp broadcast.
+
+ Returns a callback that can be used to cancel the registration.
+ """
+ scanner: Scanner = hass.data[DOMAIN]
+ return scanner.async_register_callback(callback, match_dict)
+
+
+@bind_hass
+def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name
+ hass: HomeAssistant, udn: str, st: str
+) -> dict[str, str] | None:
+ """Fetch the discovery info cache."""
+ scanner: Scanner = hass.data[DOMAIN]
+ return scanner.async_get_discovery_info_by_udn_st(udn, st)
+
+
+@bind_hass
+def async_get_discovery_info_by_st( # pylint: disable=invalid-name
+ hass: HomeAssistant, st: str
+) -> list[dict[str, str]]:
+ """Fetch all the entries matching the st."""
+ scanner: Scanner = hass.data[DOMAIN]
+ return scanner.async_get_discovery_info_by_st(st)
+
+
+@bind_hass
+def async_get_discovery_info_by_udn(
+ hass: HomeAssistant, udn: str
+) -> list[dict[str, str]]:
+ """Fetch all the entries matching the udn."""
+ scanner: Scanner = hass.data[DOMAIN]
+ return scanner.async_get_discovery_info_by_udn(udn)
+
+
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the SSDP integration."""
- async def _async_initialize(_):
- scanner = Scanner(hass, await async_get_ssdp(hass))
- await scanner.async_scan(None)
- cancel_scan = async_track_time_interval(hass, scanner.async_scan, SCAN_INTERVAL)
+ scanner = hass.data[DOMAIN] = Scanner(hass, await async_get_ssdp(hass))
- @callback
- def _async_stop_scans(event):
- cancel_scan()
+ asyncio.create_task(scanner.async_start())
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_scans)
+ return True
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_initialize)
+@core_callback
+def _async_use_default_interface(adapters: list[network.Adapter]) -> bool:
+ for adapter in adapters:
+ if adapter["enabled"] and not adapter["default"]:
+ return False
+ return True
+
+
+@core_callback
+def _async_process_callbacks(
+ callbacks: list[Callable[[dict], None]], discovery_info: dict[str, str]
+) -> None:
+ for callback in callbacks:
+ try:
+ callback(discovery_info)
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Failed to callback info: %s", discovery_info)
+
+
+@core_callback
+def _async_headers_match(
+ headers: Mapping[str, str], match_dict: dict[str, str]
+) -> bool:
+ for header, val in match_dict.items():
+ if val == MATCH_ALL:
+ if header not in headers:
+ return False
+ elif headers.get(header) != val:
+ return False
return True
class Scanner:
"""Class to manage SSDP scanning."""
- def __init__(self, hass, integration_matchers):
+ def __init__(
+ self, hass: HomeAssistant, integration_matchers: dict[str, list[dict[str, str]]]
+ ) -> None:
"""Initialize class."""
self.hass = hass
- self.seen = set()
- self._entries = []
+ self.seen: set[tuple[str, str | None]] = set()
+ self.cache: dict[tuple[str, str], Mapping[str, str]] = {}
self._integration_matchers = integration_matchers
- self._description_cache = {}
+ self._cancel_scan: Callable[[], None] | None = None
+ self._ssdp_listeners: list[SSDPListener] = []
+ self._callbacks: list[tuple[Callable[[dict], None], dict[str, str]]] = []
+ self.flow_dispatcher: FlowDispatcher | None = None
+ self.description_manager: DescriptionManager | None = None
- async def _on_ssdp_response(self, data: Mapping[str, Any]) -> None:
- """Process an ssdp response."""
- self.async_store_entry(
- ssdp.UPNPEntry({key.lower(): item for key, item in data.items()})
+ @core_callback
+ def async_register_callback(
+ self, callback: Callable[[dict], None], match_dict: None | dict[str, str] = None
+ ) -> Callable[[], None]:
+ """Register a callback."""
+ if match_dict is None:
+ match_dict = {}
+
+ # Make sure any entries that happened
+ # before the callback was registered are fired
+ if self.hass.state != CoreState.running:
+ for headers in self.cache.values():
+ if _async_headers_match(headers, match_dict):
+ _async_process_callbacks(
+ [callback], self._async_headers_to_discovery_info(headers)
+ )
+
+ callback_entry = (callback, match_dict)
+ self._callbacks.append(callback_entry)
+
+ @core_callback
+ def _async_remove_callback() -> None:
+ self._callbacks.remove(callback_entry)
+
+ return _async_remove_callback
+
+ @core_callback
+ def async_stop(self, *_: Any) -> None:
+ """Stop the scanner."""
+ assert self._cancel_scan is not None
+ self._cancel_scan()
+ for listener in self._ssdp_listeners:
+ listener.async_stop()
+ self._ssdp_listeners = []
+
+ 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 _async_use_default_interface(adapters):
+ sources.add(IPv4Address("0.0.0.0"))
+ return sources
+
+ for adapter in adapters:
+ if not adapter["enabled"]:
+ continue
+ if adapter["ipv4"]:
+ ipv4 = adapter["ipv4"][0]
+ sources.add(IPv4Address(ipv4["address"]))
+ if adapter["ipv6"]:
+ ipv6 = adapter["ipv6"][0]
+ # With python 3.9 add scope_ids can be
+ # added by enumerating adapter["ipv6"]s
+ # IPv6Address(f"::%{ipv6['scope_id']}")
+ sources.add(IPv6Address(ipv6["address"]))
+
+ return sources
+
+ @core_callback
+ def async_scan(self, *_: Any) -> None:
+ """Scan for new entries."""
+ for listener in self._ssdp_listeners:
+ listener.async_search()
+
+ async def async_start(self) -> None:
+ """Start the scanner."""
+ self.description_manager = DescriptionManager(self.hass)
+ self.flow_dispatcher = FlowDispatcher(self.hass)
+ for source_ip in await self._async_build_source_set():
+ self._ssdp_listeners.append(
+ SSDPListener(
+ async_callback=self._async_process_entry, source_ip=source_ip
+ )
+ )
+
+ self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
+ self.hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STARTED, self.flow_dispatcher.async_start
+ )
+ await asyncio.gather(
+ *[listener.async_start() for listener in self._ssdp_listeners]
+ )
+ self._cancel_scan = async_track_time_interval(
+ self.hass, self.async_scan, SCAN_INTERVAL
)
- @callback
- def async_store_entry(self, entry):
- """Save an entry for later processing."""
- self._entries.append(entry)
-
- async def async_scan(self, _):
- """Scan for new entries."""
-
- await async_search(async_callback=self._on_ssdp_response)
- await self._process_entries()
-
- # We clear the cache after each run. We track discovered entries
- # so will never need a description twice.
- self._description_cache.clear()
- self._entries.clear()
-
- async def _process_entries(self):
- """Process SSDP entries."""
- entries_to_process = []
- unseen_locations = set()
-
- for entry in self._entries:
- key = (entry.st, entry.location)
-
- if key in self.seen:
- continue
-
- self.seen.add(key)
-
- entries_to_process.append(entry)
-
- if (
- entry.location is not None
- and entry.location not in self._description_cache
- ):
- unseen_locations.add(entry.location)
-
- if not entries_to_process:
- return
-
- if unseen_locations:
- await self._fetch_descriptions(list(unseen_locations))
-
- tasks = []
-
- for entry in entries_to_process:
- info, domains = self._process_entry(entry)
- for domain in domains:
- _LOGGER.debug("Discovered %s at %s", domain, entry.location)
- tasks.append(
- self.hass.config_entries.flow.async_init(
- domain, context={"source": DOMAIN}, data=info
- )
- )
-
- if tasks:
- await asyncio.gather(*tasks)
-
- async def _fetch_descriptions(self, locations):
- """Fetch descriptions from locations."""
-
- for idx, result in enumerate(
- await asyncio.gather(
- *[self._fetch_description(location) for location in locations],
- return_exceptions=True,
- )
- ):
- location = locations[idx]
-
- if isinstance(result, Exception):
- _LOGGER.exception(
- "Failed to fetch ssdp data from: %s", location, exc_info=result
- )
- continue
-
- self._description_cache[location] = result
-
- def _process_entry(self, entry):
- """Process a single entry."""
-
- info = {"st": entry.st}
- for key in "usn", "ext", "server":
- if key in entry.values:
- info[key] = entry.values[key]
-
- if entry.location:
- # Multiple entries usually share same location. Make sure
- # we fetch it only once.
- info_req = self._description_cache.get(entry.location)
- if info_req is None:
- return (None, [])
-
- info.update(info_req)
+ @core_callback
+ def _async_get_matching_callbacks(
+ self, headers: Mapping[str, str]
+ ) -> list[Callable[[dict], None]]:
+ """Return a list of callbacks that match."""
+ return [
+ callback
+ for callback, match_dict in self._callbacks
+ if _async_headers_match(headers, match_dict)
+ ]
+ @core_callback
+ def _async_matching_domains(self, info_with_req: CaseInsensitiveDict) -> set[str]:
domains = set()
for domain, matchers in self._integration_matchers.items():
for matcher in matchers:
- if all(info.get(k) == v for (k, v) in matcher.items()):
+ if all(info_with_req.get(k) == v for (k, v) in matcher.items()):
domains.add(domain)
+ return domains
- if domains:
- return (info_from_entry(entry, info), domains)
+ def _async_seen(self, header_st: str | None, header_location: str | None) -> bool:
+ """Check if we have seen a specific st and optional location."""
+ if header_st is None:
+ return True
+ return (header_st, header_location) in self.seen
- return (None, [])
+ def _async_see(self, header_st: str | None, header_location: str | None) -> None:
+ """Mark a specific st and optional location as seen."""
+ if header_st is not None:
+ self.seen.add((header_st, header_location))
- async def _fetch_description(self, xml_location):
- """Fetch an XML description."""
- session = self.hass.helpers.aiohttp_client.async_get_clientsession()
- try:
- for _ in range(2):
- resp = await session.get(xml_location, timeout=5)
- xml = await resp.text(errors="replace")
- # Samsung Smart TV sometimes returns an empty document the
- # first time. Retry once.
- if xml:
- break
- except (aiohttp.ClientError, asyncio.TimeoutError) as err:
- _LOGGER.debug("Error fetching %s: %s", xml_location, err)
- return {}
+ async def _async_process_entry(self, headers: Mapping[str, str]) -> None:
+ """Process SSDP entries."""
+ _LOGGER.debug("_async_process_entry: %s", headers)
+ h_st = headers.get("st")
+ h_location = headers.get("location")
- try:
- tree = ElementTree.fromstring(xml)
- except ElementTree.ParseError as err:
- _LOGGER.debug("Error parsing %s: %s", xml_location, err)
- return {}
+ if h_st and (udn := _udn_from_usn(headers.get("usn"))):
+ self.cache[(udn, h_st)] = headers
- return util.etree_to_dict(tree).get("root", {}).get("device", {})
+ callbacks = self._async_get_matching_callbacks(headers)
+ if self._async_seen(h_st, h_location) and not callbacks:
+ return
+
+ assert self.description_manager is not None
+ info_req = await self.description_manager.fetch_description(h_location) or {}
+ info_with_req = CaseInsensitiveDict(**headers, **info_req)
+ discovery_info = discovery_info_from_headers_and_request(info_with_req)
+
+ _async_process_callbacks(callbacks, discovery_info)
+
+ if self._async_seen(h_st, h_location):
+ return
+ self._async_see(h_st, h_location)
+
+ for domain in self._async_matching_domains(info_with_req):
+ _LOGGER.debug("Discovered %s at %s", domain, h_location)
+ flow: SSDPFlow = {
+ "domain": domain,
+ "context": {"source": config_entries.SOURCE_SSDP},
+ "data": discovery_info,
+ }
+ assert self.flow_dispatcher is not None
+ self.flow_dispatcher.create(flow)
+
+ @core_callback
+ def _async_headers_to_discovery_info(
+ self, headers: Mapping[str, str]
+ ) -> dict[str, str]:
+ """Combine the headers and description into discovery_info.
+
+ Building this is a bit expensive so we only do it on demand.
+ """
+ assert self.description_manager is not None
+ location = headers["location"]
+ info_req = self.description_manager.async_cached_description(location) or {}
+ return discovery_info_from_headers_and_request(
+ CaseInsensitiveDict(**headers, **info_req)
+ )
+
+ @core_callback
+ def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name
+ self, udn: str, st: str
+ ) -> dict[str, str] | None:
+ """Return discovery_info for a udn and st."""
+ if headers := self.cache.get((udn, st)):
+ return self._async_headers_to_discovery_info(headers)
+ return None
+
+ @core_callback
+ def async_get_discovery_info_by_st( # pylint: disable=invalid-name
+ self, st: str
+ ) -> list[dict[str, str]]:
+ """Return matching discovery_infos for a st."""
+ return [
+ self._async_headers_to_discovery_info(headers)
+ for udn_st, headers in self.cache.items()
+ if udn_st[1] == st
+ ]
+
+ @core_callback
+ def async_get_discovery_info_by_udn(self, udn: str) -> list[dict[str, str]]:
+ """Return matching discovery_infos for a udn."""
+ return [
+ self._async_headers_to_discovery_info(headers)
+ for udn_st, headers in self.cache.items()
+ if udn_st[0] == udn
+ ]
-def info_from_entry(entry, device_info):
- """Get info from an entry."""
- info = {
- ATTR_SSDP_LOCATION: entry.location,
- ATTR_SSDP_ST: entry.st,
- }
- if device_info:
- info.update(device_info)
- info.pop("st", None)
- if "usn" in info:
- info[ATTR_SSDP_USN] = info.pop("usn")
- if "ext" in info:
- info[ATTR_SSDP_EXT] = info.pop("ext")
- if "server" in info:
- info[ATTR_SSDP_SERVER] = info.pop("server")
+def discovery_info_from_headers_and_request(
+ info_with_req: CaseInsensitiveDict,
+) -> dict[str, str]:
+ """Convert headers and description to discovery_info."""
+ info = {DISCOVERY_MAPPING.get(k.lower(), k): v for k, v in info_with_req.items()}
+
+ if ATTR_UPNP_UDN not in info and ATTR_SSDP_USN in info:
+ if udn := _udn_from_usn(info[ATTR_SSDP_USN]):
+ info[ATTR_UPNP_UDN] = udn
return info
+
+
+def _udn_from_usn(usn: str | None) -> str | None:
+ """Get the UDN from the USN."""
+ if usn is None:
+ return None
+ if usn.startswith("uuid:"):
+ return usn.split("::")[0]
+ return None
diff --git a/homeassistant/components/ssdp/descriptions.py b/homeassistant/components/ssdp/descriptions.py
new file mode 100644
index 00000000000..e754b10669a
--- /dev/null
+++ b/homeassistant/components/ssdp/descriptions.py
@@ -0,0 +1,69 @@
+"""The SSDP integration."""
+from __future__ import annotations
+
+import asyncio
+import logging
+
+import aiohttp
+from defusedxml import ElementTree
+
+from homeassistant.core import HomeAssistant, callback
+
+from .util import etree_to_dict
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class DescriptionManager:
+ """Class to cache and manage fetching descriptions."""
+
+ def __init__(self, hass: HomeAssistant) -> None:
+ """Init the manager."""
+ self.hass = hass
+ self._description_cache: dict[str, None | dict[str, str]] = {}
+
+ async def fetch_description(
+ self, xml_location: str | None
+ ) -> None | dict[str, str]:
+ """Fetch the location or get it from the cache."""
+ if xml_location is None:
+ return None
+ if xml_location not in self._description_cache:
+ try:
+ self._description_cache[xml_location] = await self._fetch_description(
+ xml_location
+ )
+ except Exception: # pylint: disable=broad-except
+ # If it fails, cache the failure so we do not keep trying over and over
+ self._description_cache[xml_location] = None
+ _LOGGER.exception("Failed to fetch ssdp data from: %s", xml_location)
+
+ return self._description_cache[xml_location]
+
+ @callback
+ def async_cached_description(self, xml_location: str) -> None | dict[str, str]:
+ """Fetch the description from the cache."""
+ return self._description_cache.get(xml_location)
+
+ async def _fetch_description(self, xml_location: str) -> None | dict[str, str]:
+ """Fetch an XML description."""
+ session = self.hass.helpers.aiohttp_client.async_get_clientsession()
+ try:
+ for _ in range(2):
+ resp = await session.get(xml_location, timeout=5)
+ # Samsung Smart TV sometimes returns an empty document the
+ # first time. Retry once.
+ if xml := await resp.text(errors="replace"):
+ break
+ except (aiohttp.ClientError, asyncio.TimeoutError) as err:
+ _LOGGER.debug("Error fetching %s: %s", xml_location, err)
+ return None
+
+ try:
+ tree = ElementTree.fromstring(xml)
+ except ElementTree.ParseError as err:
+ _LOGGER.debug("Error parsing %s: %s", xml_location, err)
+ return None
+
+ root = etree_to_dict(tree).get("root") or {}
+ return root.get("device") or {}
diff --git a/homeassistant/components/ssdp/flow.py b/homeassistant/components/ssdp/flow.py
new file mode 100644
index 00000000000..77f4cb107b8
--- /dev/null
+++ b/homeassistant/components/ssdp/flow.py
@@ -0,0 +1,50 @@
+"""The SSDP integration."""
+from __future__ import annotations
+
+from collections.abc import Coroutine
+from typing import Any, TypedDict
+
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.data_entry_flow import FlowResult
+
+
+class SSDPFlow(TypedDict):
+ """A queued ssdp discovery flow."""
+
+ domain: str
+ context: dict[str, Any]
+ data: dict
+
+
+class FlowDispatcher:
+ """Dispatch discovery flows."""
+
+ def __init__(self, hass: HomeAssistant) -> None:
+ """Init the discovery dispatcher."""
+ self.hass = hass
+ self.pending_flows: list[SSDPFlow] = []
+ self.started = False
+
+ @callback
+ def async_start(self, *_: Any) -> None:
+ """Start processing pending flows."""
+ self.started = True
+ self.hass.loop.call_soon(self._async_process_pending_flows)
+
+ def _async_process_pending_flows(self) -> None:
+ for flow in self.pending_flows:
+ self.hass.async_create_task(self._init_flow(flow))
+ self.pending_flows = []
+
+ def create(self, flow: SSDPFlow) -> None:
+ """Create and add or queue a flow."""
+ if self.started:
+ self.hass.async_create_task(self._init_flow(flow))
+ else:
+ self.pending_flows.append(flow)
+
+ def _init_flow(self, flow: SSDPFlow) -> Coroutine[None, None, FlowResult]:
+ """Create a flow."""
+ return self.hass.config_entries.flow.async_init(
+ flow["domain"], context=flow["context"], data=flow["data"]
+ )
diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json
index d3dbc0c920e..faadfac5c0c 100644
--- a/homeassistant/components/ssdp/manifest.json
+++ b/homeassistant/components/ssdp/manifest.json
@@ -4,9 +4,9 @@
"documentation": "https://www.home-assistant.io/integrations/ssdp",
"requirements": [
"defusedxml==0.7.1",
- "netdisco==2.8.3",
- "async-upnp-client==0.18.0"
+ "async-upnp-client==0.19.0"
],
+ "dependencies": ["network"],
"after_dependencies": ["zeroconf"],
"codeowners": [],
"quality_scale": "internal",
diff --git a/homeassistant/components/ssdp/util.py b/homeassistant/components/ssdp/util.py
new file mode 100644
index 00000000000..c28f8ce088d
--- /dev/null
+++ b/homeassistant/components/ssdp/util.py
@@ -0,0 +1,42 @@
+"""Util functions used by SSDP."""
+from __future__ import annotations
+
+from collections import defaultdict
+from typing import Any
+
+from defusedxml import ElementTree
+
+
+# Adapted from http://stackoverflow.com/a/10077069
+# to follow the XML to JSON spec
+# https://www.xml.com/pub/a/2006/05/31/converting-between-xml-and-json.html
+def etree_to_dict(tree: ElementTree) -> dict[str, dict[str, Any] | None]:
+ """Convert an ETree object to a dict."""
+ # strip namespace
+ tag_name = tree.tag[tree.tag.find("}") + 1 :]
+
+ tree_dict: dict[str, dict[str, Any] | None] = {
+ tag_name: {} if tree.attrib else None
+ }
+ children = list(tree)
+ if children:
+ child_dict: dict[str, list] = defaultdict(list)
+ for child in map(etree_to_dict, children):
+ for k, val in child.items():
+ child_dict[k].append(val)
+ tree_dict = {
+ tag_name: {k: v[0] if len(v) == 1 else v for k, v in child_dict.items()}
+ }
+ dict_meta = tree_dict[tag_name]
+ if tree.attrib:
+ assert dict_meta is not None
+ dict_meta.update(("@" + k, v) for k, v in tree.attrib.items())
+ if tree.text:
+ text = tree.text.strip()
+ if children or tree.attrib:
+ if text:
+ assert dict_meta is not None
+ dict_meta["#text"] = text
+ else:
+ tree_dict[tag_name] = text
+ return tree_dict
diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py
index 91edc7badeb..0a8bf7e05f8 100644
--- a/homeassistant/components/starline/__init__.py
+++ b/homeassistant/components/starline/__init__.py
@@ -19,9 +19,9 @@ from .const import (
)
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the StarLine device from a config entry."""
- account = StarlineAccount(hass, config_entry)
+ account = StarlineAccount(hass, entry)
await account.update()
await account.update_obd()
if not account.api.available:
@@ -29,27 +29,27 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
- hass.data[DOMAIN][config_entry.entry_id] = account
+ hass.data[DOMAIN][entry.entry_id] = account
device_registry = await hass.helpers.device_registry.async_get_registry()
for device in account.api.devices.values():
device_registry.async_get_or_create(
- config_entry_id=config_entry.entry_id, **account.device_info(device)
+ config_entry_id=entry.entry_id, **account.device_info(device)
)
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
async def async_set_scan_interval(call):
"""Set scan interval."""
- options = dict(config_entry.options)
+ options = dict(entry.options)
options[CONF_SCAN_INTERVAL] = call.data[CONF_SCAN_INTERVAL]
- hass.config_entries.async_update_entry(entry=config_entry, options=options)
+ hass.config_entries.async_update_entry(entry=entry, options=options)
async def async_set_scan_obd_interval(call):
"""Set OBD info scan interval."""
- options = dict(config_entry.options)
+ options = dict(entry.options)
options[CONF_SCAN_OBD_INTERVAL] = call.data[CONF_SCAN_INTERVAL]
- hass.config_entries.async_update_entry(entry=config_entry, options=options)
+ hass.config_entries.async_update_entry(entry=entry, options=options)
async def async_update(call=None):
"""Update all data."""
@@ -82,10 +82,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
),
)
- config_entry.async_on_unload(
- config_entry.add_update_listener(async_options_updated)
- )
- await async_options_updated(hass, config_entry)
+ entry.async_on_unload(entry.add_update_listener(async_options_updated))
+ await async_options_updated(hass, entry)
return True
diff --git a/homeassistant/components/starline/translations/de.json b/homeassistant/components/starline/translations/de.json
index 5788b570eba..87a9249475e 100644
--- a/homeassistant/components/starline/translations/de.json
+++ b/homeassistant/components/starline/translations/de.json
@@ -11,7 +11,7 @@
"app_id": "App-ID",
"app_secret": "Geheimnis"
},
- "description": "Anwendungs-ID und Geheimcode aus dem StarLine-Entwicklerkonto",
+ "description": "Anwendungs-ID und Geheimcode aus dem [StarLine Entwicklerkonto](https://my.starline.ru/developer)",
"title": "Anmeldeinformationen der Anwendung"
},
"auth_captcha": {
diff --git a/homeassistant/components/starline/translations/he.json b/homeassistant/components/starline/translations/he.json
index 53542798232..06aacb32caa 100644
--- a/homeassistant/components/starline/translations/he.json
+++ b/homeassistant/components/starline/translations/he.json
@@ -1,11 +1,18 @@
{
"config": {
+ "error": {
+ "error_auth_user": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05d0\u05d5 \u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d9\u05dd"
+ },
"step": {
+ "auth_captcha": {
+ "description": "{captcha_img}"
+ },
"auth_user": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
- }
+ },
+ "description": "\u05d3\u05d5\u05d0\"\u05dc \u05d5\u05e1\u05d9\u05e1\u05de\u05d4 \u05dc\u05d7\u05e9\u05d1\u05d5\u05df StarLine"
}
}
}
diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py
index e32ae0debaf..b1ea6cfb50f 100644
--- a/homeassistant/components/statistics/sensor.py
+++ b/homeassistant/components/statistics/sensor.py
@@ -39,6 +39,7 @@ ATTR_MEAN = "mean"
ATTR_MEDIAN = "median"
ATTR_MIN_AGE = "min_age"
ATTR_MIN_VALUE = "min_value"
+ATTR_QUANTILES = "quantiles"
ATTR_SAMPLING_SIZE = "sampling_size"
ATTR_STANDARD_DEVIATION = "standard_deviation"
ATTR_TOTAL = "total"
@@ -47,10 +48,14 @@ ATTR_VARIANCE = "variance"
CONF_SAMPLING_SIZE = "sampling_size"
CONF_MAX_AGE = "max_age"
CONF_PRECISION = "precision"
+CONF_QUANTILE_INTERVALS = "quantile_intervals"
+CONF_QUANTILE_METHOD = "quantile_method"
DEFAULT_NAME = "Stats"
DEFAULT_SIZE = 20
DEFAULT_PRECISION = 2
+DEFAULT_QUANTILE_INTERVALS = 4
+DEFAULT_QUANTILE_METHOD = "exclusive"
ICON = "mdi:calculator"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
@@ -62,6 +67,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
),
vol.Optional(CONF_MAX_AGE): cv.time_period,
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int),
+ vol.Optional(
+ CONF_QUANTILE_INTERVALS, default=DEFAULT_QUANTILE_INTERVALS
+ ): vol.All(vol.Coerce(int), vol.Range(min=2)),
+ vol.Optional(CONF_QUANTILE_METHOD, default=DEFAULT_QUANTILE_METHOD): vol.In(
+ ["exclusive", "inclusive"]
+ ),
}
)
@@ -76,9 +87,22 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
sampling_size = config.get(CONF_SAMPLING_SIZE)
max_age = config.get(CONF_MAX_AGE)
precision = config.get(CONF_PRECISION)
+ quantile_intervals = config.get(CONF_QUANTILE_INTERVALS)
+ quantile_method = config.get(CONF_QUANTILE_METHOD)
async_add_entities(
- [StatisticsSensor(entity_id, name, sampling_size, max_age, precision)], True
+ [
+ StatisticsSensor(
+ entity_id,
+ name,
+ sampling_size,
+ max_age,
+ precision,
+ quantile_intervals,
+ quantile_method,
+ )
+ ],
+ True,
)
return True
@@ -87,7 +111,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class StatisticsSensor(SensorEntity):
"""Representation of a Statistics sensor."""
- def __init__(self, entity_id, name, sampling_size, max_age, precision):
+ def __init__(
+ self,
+ entity_id,
+ name,
+ sampling_size,
+ max_age,
+ precision,
+ quantile_intervals,
+ quantile_method,
+ ):
"""Initialize the Statistics sensor."""
self._entity_id = entity_id
self.is_binary = self._entity_id.split(".")[0] == "binary_sensor"
@@ -95,12 +128,14 @@ class StatisticsSensor(SensorEntity):
self._sampling_size = sampling_size
self._max_age = max_age
self._precision = precision
+ self._quantile_intervals = quantile_intervals
+ self._quantile_method = quantile_method
self._unit_of_measurement = None
self.states = deque(maxlen=self._sampling_size)
self.ages = deque(maxlen=self._sampling_size)
self.count = 0
- self.mean = self.median = self.stdev = self.variance = None
+ self.mean = self.median = self.quantiles = self.stdev = self.variance = None
self.total = self.min = self.max = None
self.min_age = self.max_age = None
self.change = self.average_change = self.change_rate = None
@@ -191,6 +226,7 @@ class StatisticsSensor(SensorEntity):
ATTR_COUNT: self.count,
ATTR_MEAN: self.mean,
ATTR_MEDIAN: self.median,
+ ATTR_QUANTILES: self.quantiles,
ATTR_STANDARD_DEVIATION: self.stdev,
ATTR_VARIANCE: self.variance,
ATTR_TOTAL: self.total,
@@ -257,9 +293,18 @@ class StatisticsSensor(SensorEntity):
try: # require at least two data points
self.stdev = round(statistics.stdev(self.states), self._precision)
self.variance = round(statistics.variance(self.states), self._precision)
+ if self._quantile_intervals < self.count:
+ self.quantiles = [
+ round(quantile, self._precision)
+ for quantile in statistics.quantiles(
+ self.states,
+ n=self._quantile_intervals,
+ method=self._quantile_method,
+ )
+ ]
except statistics.StatisticsError as err:
_LOGGER.debug("%s: %s", self.entity_id, err)
- self.stdev = self.variance = STATE_UNKNOWN
+ self.stdev = self.variance = self.quantiles = STATE_UNKNOWN
if self.states:
self.total = round(sum(self.states), self._precision)
diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py
index 67bfe404d7d..d8e4cb2cdb2 100644
--- a/homeassistant/components/stream/__init__.py
+++ b/homeassistant/components/stream/__init__.py
@@ -16,40 +16,48 @@ to always keep workers active.
"""
from __future__ import annotations
+from collections.abc import Mapping
import logging
import re
import secrets
import threading
import time
from types import MappingProxyType
+from typing import cast
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
-from homeassistant.core import callback
+from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.typing import ConfigType
from .const import (
ATTR_ENDPOINTS,
ATTR_STREAMS,
DOMAIN,
+ HLS_PROVIDER,
MAX_SEGMENTS,
OUTPUT_IDLE_TIMEOUT,
+ RECORDER_PROVIDER,
STREAM_RESTART_INCREMENT,
STREAM_RESTART_RESET_TIME,
)
from .core import PROVIDERS, IdleTimer, StreamOutput
from .hls import async_setup_hls
+from .recorder import RecorderOutput
_LOGGER = logging.getLogger(__name__)
STREAM_SOURCE_RE = re.compile("//.*:.*@")
-def redact_credentials(data):
+def redact_credentials(data: str) -> str:
"""Redact credentials from string data."""
return STREAM_SOURCE_RE.sub("//****:****@", data)
-def create_stream(hass, stream_source, options=None):
+def create_stream(
+ hass: HomeAssistant, stream_source: str, options: dict[str, str]
+) -> Stream:
"""Create a stream with the specified identfier based on the source url.
The stream_source is typically an rtsp url and options are passed into
@@ -58,9 +66,6 @@ def create_stream(hass, stream_source, options=None):
if DOMAIN not in hass.config.components:
raise HomeAssistantError("Stream integration is not set up.")
- if options is None:
- options = {}
-
# For RTSP streams, prefer TCP
if isinstance(stream_source, str) and stream_source[:7] == "rtsp://":
options = {
@@ -74,7 +79,7 @@ def create_stream(hass, stream_source, options=None):
return stream
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up stream."""
# Set log level to error for libav
logging.getLogger("libav").setLevel(logging.ERROR)
@@ -90,13 +95,13 @@ async def async_setup(hass, config):
# Setup HLS
hls_endpoint = async_setup_hls(hass)
- hass.data[DOMAIN][ATTR_ENDPOINTS]["hls"] = hls_endpoint
+ hass.data[DOMAIN][ATTR_ENDPOINTS][HLS_PROVIDER] = hls_endpoint
# Setup Recorder
async_setup_recorder(hass)
@callback
- def shutdown(event):
+ def shutdown(event: Event) -> None:
"""Stop all stream workers."""
for stream in hass.data[DOMAIN][ATTR_STREAMS]:
stream.keepalive = False
@@ -111,42 +116,46 @@ async def async_setup(hass, config):
class Stream:
"""Represents a single stream."""
- def __init__(self, hass, source, options=None):
+ def __init__(
+ self, hass: HomeAssistant, source: str, options: dict[str, str]
+ ) -> None:
"""Initialize a stream."""
self.hass = hass
self.source = source
self.options = options
self.keepalive = False
- self.access_token = None
- self._thread = None
+ self.access_token: str | None = None
+ self._thread: threading.Thread | None = None
self._thread_quit = threading.Event()
self._outputs: dict[str, StreamOutput] = {}
self._fast_restart_once = False
- if self.options is None:
- self.options = {}
-
def endpoint_url(self, fmt: str) -> str:
"""Start the stream and returns a url for the output format."""
if fmt not in self._outputs:
raise ValueError(f"Stream is not configured for format '{fmt}'")
if not self.access_token:
self.access_token = secrets.token_hex()
- return self.hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(self.access_token)
+ endpoint_fmt: str = self.hass.data[DOMAIN][ATTR_ENDPOINTS][fmt]
+ return endpoint_fmt.format(self.access_token)
- def outputs(self):
+ def outputs(self) -> Mapping[str, StreamOutput]:
"""Return a copy of the stream outputs."""
# A copy is returned so the caller can iterate through the outputs
# without concern about self._outputs being modified from another thread.
return MappingProxyType(self._outputs.copy())
- def add_provider(self, fmt, timeout=OUTPUT_IDLE_TIMEOUT):
+ def add_provider(
+ self, fmt: str, timeout: int = OUTPUT_IDLE_TIMEOUT
+ ) -> StreamOutput:
"""Add provider output stream."""
if not self._outputs.get(fmt):
@callback
- def idle_callback():
- if (not self.keepalive or fmt == "recorder") and fmt in self._outputs:
+ def idle_callback() -> None:
+ if (
+ not self.keepalive or fmt == RECORDER_PROVIDER
+ ) and fmt in self._outputs:
self.remove_provider(self._outputs[fmt])
self.check_idle()
@@ -156,7 +165,7 @@ class Stream:
self._outputs[fmt] = provider
return self._outputs[fmt]
- def remove_provider(self, provider):
+ def remove_provider(self, provider: StreamOutput) -> None:
"""Remove provider output stream."""
if provider.name in self._outputs:
self._outputs[provider.name].cleanup()
@@ -165,12 +174,12 @@ class Stream:
if not self._outputs:
self.stop()
- def check_idle(self):
+ def check_idle(self) -> None:
"""Reset access token if all providers are idle."""
if all(p.idle for p in self._outputs.values()):
self.access_token = None
- def start(self):
+ def start(self) -> None:
"""Start a stream."""
if self._thread is None or not self._thread.is_alive():
if self._thread is not None:
@@ -185,14 +194,14 @@ class Stream:
self._thread.start()
_LOGGER.info("Started stream: %s", redact_credentials(str(self.source)))
- def update_source(self, new_source):
+ def update_source(self, new_source: str) -> None:
"""Restart the stream with a new stream source."""
_LOGGER.debug("Updating stream source %s", new_source)
self.source = new_source
self._fast_restart_once = True
self._thread_quit.set()
- def _run_worker(self):
+ def _run_worker(self) -> None:
"""Handle consuming streams and restart keepalive streams."""
# Keep import here so that we can import stream integration without installing reqs
# pylint: disable=import-outside-toplevel
@@ -225,17 +234,17 @@ class Stream:
)
self._worker_finished()
- def _worker_finished(self):
+ def _worker_finished(self) -> None:
"""Schedule cleanup of all outputs."""
@callback
- def remove_outputs():
+ def remove_outputs() -> None:
for provider in self.outputs().values():
self.remove_provider(provider)
self.hass.loop.call_soon_threadsafe(remove_outputs)
- def stop(self):
+ def stop(self) -> None:
"""Remove outputs and access token."""
self._outputs = {}
self.access_token = None
@@ -243,7 +252,7 @@ class Stream:
if not self.keepalive:
self._stop()
- def _stop(self):
+ def _stop(self) -> None:
"""Stop worker thread."""
if self._thread is not None:
self._thread_quit.set()
@@ -251,7 +260,9 @@ class Stream:
self._thread = None
_LOGGER.info("Stopped stream: %s", redact_credentials(str(self.source)))
- async def async_record(self, video_path, duration=30, lookback=5):
+ async def async_record(
+ self, video_path: str, duration: int = 30, lookback: int = 5
+ ) -> None:
"""Make a .mp4 recording from a provided stream."""
# Check for file access
@@ -259,19 +270,22 @@ class Stream:
raise HomeAssistantError(f"Can't write {video_path}, no access to path!")
# Add recorder
- recorder = self.outputs().get("recorder")
+ recorder = self.outputs().get(RECORDER_PROVIDER)
if recorder:
+ assert isinstance(recorder, RecorderOutput)
raise HomeAssistantError(
f"Stream already recording to {recorder.video_path}!"
)
- recorder = self.add_provider("recorder", timeout=duration)
+ recorder = cast(
+ RecorderOutput, self.add_provider(RECORDER_PROVIDER, timeout=duration)
+ )
recorder.video_path = video_path
self.start()
_LOGGER.debug("Started a stream recording of %s seconds", duration)
# Take advantage of lookback
- hls = self.outputs().get("hls")
+ hls = self.outputs().get(HLS_PROVIDER)
if lookback > 0 and hls:
num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS)
# Wait for latest segment, then add the lookback
diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py
index a2557286cf1..cf4a80d9705 100644
--- a/homeassistant/components/stream/const.py
+++ b/homeassistant/components/stream/const.py
@@ -4,25 +4,37 @@ DOMAIN = "stream"
ATTR_ENDPOINTS = "endpoints"
ATTR_STREAMS = "streams"
-OUTPUT_FORMATS = ["hls"]
+HLS_PROVIDER = "hls"
+RECORDER_PROVIDER = "recorder"
+
+OUTPUT_FORMATS = [HLS_PROVIDER]
SEGMENT_CONTAINER_FORMAT = "mp4" # format for segments
RECORDER_CONTAINER_FORMAT = "mp4" # format for recorder output
AUDIO_CODECS = {"aac", "mp3"}
-FORMAT_CONTENT_TYPE = {"hls": "application/vnd.apple.mpegurl"}
+FORMAT_CONTENT_TYPE = {HLS_PROVIDER: "application/vnd.apple.mpegurl"}
OUTPUT_IDLE_TIMEOUT = 300 # Idle timeout due to inactivity
NUM_PLAYLIST_SEGMENTS = 3 # Number of segments to use in HLS playlist
-MAX_SEGMENTS = 4 # Max number of segments to keep around
-MIN_SEGMENT_DURATION = 1.5 # Each segment is at least this many seconds
+MAX_SEGMENTS = 5 # Max number of segments to keep around
+TARGET_SEGMENT_DURATION = 2.0 # Each segment is about this many seconds
+TARGET_PART_DURATION = 1.0
+SEGMENT_DURATION_ADJUSTER = 0.1 # Used to avoid missing keyframe boundaries
+# Each segment is at least this many seconds
+MIN_SEGMENT_DURATION = TARGET_SEGMENT_DURATION - SEGMENT_DURATION_ADJUSTER
+
+# Number of target durations to start before the end of the playlist.
+# 1.5 should put us in the middle of the second to last segment even with
+# variable keyframe intervals.
+EXT_X_START = 1.5
PACKETS_TO_WAIT_FOR_AUDIO = 20 # Some streams have an audio stream with no audio
MAX_TIMESTAMP_GAP = 10000 # seconds - anything from 10 to 50000 is probably reasonable
MAX_MISSING_DTS = 6 # Number of packets missing DTS to allow
-STREAM_TIMEOUT = 30 # Timeout for reading stream
+SOURCE_TIMEOUT = 30 # Timeout for reading stream source
STREAM_RESTART_INCREMENT = 10 # Increase wait_timeout by this amount each retry
STREAM_RESTART_RESET_TIME = 300 # Reset wait_timeout after this many seconds
diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py
index 695f1d05ac3..d840bfaf858 100644
--- a/homeassistant/components/stream/core.py
+++ b/homeassistant/components/stream/core.py
@@ -3,33 +3,56 @@ from __future__ import annotations
import asyncio
from collections import deque
-from typing import Callable
+import datetime
+from typing import TYPE_CHECKING
from aiohttp import web
import attr
-from homeassistant.components.http import HomeAssistantView
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.components.http.view import HomeAssistantView
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.event import async_call_later
from homeassistant.util.decorator import Registry
-from .const import ATTR_STREAMS, DOMAIN
+from .const import ATTR_STREAMS, DOMAIN, TARGET_SEGMENT_DURATION
+
+if TYPE_CHECKING:
+ from . import Stream
PROVIDERS = Registry()
+@attr.s(slots=True)
+class Part:
+ """Represent a segment part."""
+
+ duration: float = attr.ib()
+ has_keyframe: bool = attr.ib()
+ # video data (moof+mdat)
+ data: bytes = attr.ib()
+
+
@attr.s(slots=True)
class Segment:
"""Represent a segment."""
- sequence: int = attr.ib()
- # the init of the mp4
- init: bytes = attr.ib()
- # the video data (moof + mddat)s of the mp4
- moof_data: bytes = attr.ib()
- duration: float = attr.ib()
+ sequence: int = attr.ib(default=0)
+ # the init of the mp4 the segment is based on
+ init: bytes = attr.ib(default=None)
+ duration: float = attr.ib(default=0)
# For detecting discontinuities across stream restarts
stream_id: int = attr.ib(default=0)
+ parts: list[Part] = attr.ib(factory=list)
+ start_time: datetime.datetime = attr.ib(factory=datetime.datetime.utcnow)
+
+ @property
+ def complete(self) -> bool:
+ """Return whether the Segment is complete."""
+ return self.duration > 0
+
+ def get_bytes_without_init(self) -> bytes:
+ """Return reconstructed data for all parts as bytes, without init."""
+ return b"".join([part.data for part in self.parts])
class IdleTimer:
@@ -40,34 +63,34 @@ class IdleTimer:
"""
def __init__(
- self, hass: HomeAssistant, timeout: int, idle_callback: Callable[[], None]
+ self, hass: HomeAssistant, timeout: int, idle_callback: CALLBACK_TYPE
) -> None:
"""Initialize IdleTimer."""
self._hass = hass
self._timeout = timeout
self._callback = idle_callback
- self._unsub = None
+ self._unsub: CALLBACK_TYPE | None = None
self.idle = False
- def start(self):
+ def start(self) -> None:
"""Start the idle timer if not already started."""
self.idle = False
if self._unsub is None:
self._unsub = async_call_later(self._hass, self._timeout, self.fire)
- def awake(self):
+ def awake(self) -> None:
"""Keep the idle time alive by resetting the timeout."""
self.idle = False
# Reset idle timeout
self.clear()
self._unsub = async_call_later(self._hass, self._timeout, self.fire)
- def clear(self):
+ def clear(self) -> None:
"""Clear and disable the timer if it has not already fired."""
if self._unsub is not None:
self._unsub()
- def fire(self, _now=None):
+ def fire(self, _now: datetime.datetime) -> None:
"""Invoke the idle timeout callback, called when the alarm fires."""
self.idle = True
self._unsub = None
@@ -78,12 +101,14 @@ class StreamOutput:
"""Represents a stream output."""
def __init__(
- self, hass: HomeAssistant, idle_timer: IdleTimer, deque_maxlen: int = None
+ self,
+ hass: HomeAssistant,
+ idle_timer: IdleTimer,
+ deque_maxlen: int | None = None,
) -> None:
"""Initialize a stream output."""
self._hass = hass
- self._idle_timer = idle_timer
- self._cursor: int | None = None
+ self.idle_timer = idle_timer
self._event = asyncio.Event()
self._segments: deque[Segment] = deque(maxlen=deque_maxlen)
@@ -95,48 +120,50 @@ class StreamOutput:
@property
def idle(self) -> bool:
"""Return True if the output is idle."""
- return self._idle_timer.idle
+ return self.idle_timer.idle
@property
- def segments(self) -> list[int]:
+ def last_sequence(self) -> int:
+ """Return the last sequence number without iterating."""
+ if self._segments:
+ return self._segments[-1].sequence
+ return -1
+
+ @property
+ def sequences(self) -> list[int]:
"""Return current sequence from segments."""
return [s.sequence for s in self._segments]
@property
- def target_duration(self) -> int:
+ def last_segment(self) -> Segment | None:
+ """Return the last segment without iterating."""
+ if self._segments:
+ return self._segments[-1]
+ return None
+
+ @property
+ def target_duration(self) -> float:
"""Return the max duration of any given segment in seconds."""
- segment_length = len(self._segments)
- if not segment_length:
- return 1
- durations = [s.duration for s in self._segments]
- return round(max(durations)) or 1
+ if not (durations := [s.duration for s in self._segments if s.complete]):
+ return TARGET_SEGMENT_DURATION
+ return max(durations)
def get_segment(self, sequence: int) -> Segment | None:
"""Retrieve a specific segment."""
- self._idle_timer.awake()
-
- for segment in self._segments:
+ # Most hits will come in the most recent segments, so iterate reversed
+ for segment in reversed(self._segments):
if segment.sequence == sequence:
return segment
return None
def get_segments(self) -> deque[Segment]:
"""Retrieve all segments."""
- self._idle_timer.awake()
return self._segments
- async def recv(self) -> Segment | None:
+ async def recv(self) -> bool:
"""Wait for and retrieve the latest segment."""
- last_segment = max(self.segments, default=0)
- if self._cursor is None or self._cursor <= last_segment:
- await self._event.wait()
-
- if not self._segments:
- return None
-
- segment = self.get_segments()[-1]
- self._cursor = segment.sequence
- return segment
+ await self._event.wait()
+ return self.last_segment is not None
def put(self, segment: Segment) -> None:
"""Store output."""
@@ -146,15 +173,15 @@ class StreamOutput:
def _async_put(self, segment: Segment) -> None:
"""Store output from event loop."""
# Start idle timeout when we start receiving data
- self._idle_timer.start()
+ self.idle_timer.start()
self._segments.append(segment)
self._event.set()
self._event.clear()
- def cleanup(self):
+ def cleanup(self) -> None:
"""Handle cleanup."""
self._event.set()
- self._idle_timer.clear()
+ self.idle_timer.clear()
self._segments = deque(maxlen=self._segments.maxlen)
@@ -169,7 +196,9 @@ class StreamView(HomeAssistantView):
requires_auth = False
platform = None
- async def get(self, request, token, sequence=None):
+ async def get(
+ self, request: web.Request, token: str, sequence: str = ""
+ ) -> web.StreamResponse:
"""Start a GET request."""
hass = request.app["hass"]
@@ -186,6 +215,8 @@ class StreamView(HomeAssistantView):
return await self.handle(request, stream, sequence)
- async def handle(self, request, stream, sequence):
+ async def handle(
+ self, request: web.Request, stream: Stream, sequence: str
+ ) -> web.StreamResponse:
"""Handle the stream request."""
raise NotImplementedError()
diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py
index 511bbc0939a..f136784cf87 100644
--- a/homeassistant/components/stream/fmp4utils.py
+++ b/homeassistant/components/stream/fmp4utils.py
@@ -5,7 +5,7 @@ from collections.abc import Generator
def find_box(
- mp4_bytes: bytes | memoryview, target_type: bytes, box_start: int = 0
+ mp4_bytes: bytes, target_type: bytes, box_start: int = 0
) -> Generator[int, None, None]:
"""Find location of first box (or sub_box if box_start provided) of given type."""
if box_start == 0:
@@ -25,16 +25,6 @@ def find_box(
index += int.from_bytes(box_header[0:4], byteorder="big")
-def get_init_and_moof_data(segment: memoryview) -> tuple[bytes, bytes]:
- """Get the init and moof data from a segment."""
- moof_location = next(find_box(segment, b"moof"), 0)
- mfra_location = next(find_box(segment, b"mfra"), len(segment))
- return (
- segment[:moof_location].tobytes(),
- segment[moof_location:mfra_location].tobytes(),
- )
-
-
def get_codec_string(mp4_bytes: bytes) -> str:
"""Get RFC 6381 codec string."""
codecs = []
diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py
index 941f4407423..7f11bc09655 100644
--- a/homeassistant/components/stream/hls.py
+++ b/homeassistant/components/stream/hls.py
@@ -1,15 +1,28 @@
"""Provide functionality to stream HLS."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
from aiohttp import web
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
-from .const import FORMAT_CONTENT_TYPE, MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS
-from .core import PROVIDERS, HomeAssistant, IdleTimer, StreamOutput, StreamView
+from .const import (
+ EXT_X_START,
+ FORMAT_CONTENT_TYPE,
+ HLS_PROVIDER,
+ MAX_SEGMENTS,
+ NUM_PLAYLIST_SEGMENTS,
+)
+from .core import PROVIDERS, IdleTimer, StreamOutput, StreamView
from .fmp4utils import get_codec_string
+if TYPE_CHECKING:
+ from . import Stream
+
@callback
-def async_setup_hls(hass):
+def async_setup_hls(hass: HomeAssistant) -> str:
"""Set up api endpoints."""
hass.http.register_view(HlsPlaylistView())
hass.http.register_view(HlsSegmentView())
@@ -26,14 +39,18 @@ class HlsMasterPlaylistView(StreamView):
cors_allowed = True
@staticmethod
- def render(track):
+ def render(track: StreamOutput) -> str:
"""Render M3U8 file."""
# Need to calculate max bandwidth as input_container.bit_rate doesn't seem to work
# Calculate file size / duration and use a small multiplier to account for variation
# hls spec already allows for 25% variation
- segment = track.get_segment(track.segments[-1])
+ if not (segment := track.get_segment(track.sequences[-2])):
+ return ""
bandwidth = round(
- (len(segment.init) + len(segment.moof_data)) * 8 / segment.duration * 1.2
+ (len(segment.init) + sum(len(part.data) for part in segment.parts))
+ * 8
+ / segment.duration
+ * 1.2
)
codecs = get_codec_string(segment.init)
lines = [
@@ -43,14 +60,18 @@ class HlsMasterPlaylistView(StreamView):
]
return "\n".join(lines) + "\n"
- async def handle(self, request, stream, sequence):
+ async def handle(
+ self, request: web.Request, stream: Stream, sequence: str
+ ) -> web.Response:
"""Return m3u8 playlist."""
- track = stream.add_provider("hls")
+ track = stream.add_provider(HLS_PROVIDER)
stream.start()
- # Wait for a segment to be ready
- if not track.segments and not await track.recv():
+ # Make sure at least two segments are ready (last one may not be complete)
+ if not track.sequences and not await track.recv():
return web.HTTPNotFound()
- headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]}
+ if len(track.sequences) == 1 and not await track.recv():
+ return web.HTTPNotFound()
+ headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]}
return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
@@ -62,55 +83,80 @@ class HlsPlaylistView(StreamView):
cors_allowed = True
@staticmethod
- def render_preamble(track):
- """Render preamble."""
- return [
- "#EXT-X-VERSION:7",
- f"#EXT-X-TARGETDURATION:{track.target_duration}",
- '#EXT-X-MAP:URI="init.mp4"',
- ]
-
- @staticmethod
- def render_playlist(track):
+ def render(track: StreamOutput) -> str:
"""Render playlist."""
- segments = list(track.get_segments())[-NUM_PLAYLIST_SEGMENTS:]
+ # NUM_PLAYLIST_SEGMENTS+1 because most recent is probably not yet complete
+ segments = list(track.get_segments())[-(NUM_PLAYLIST_SEGMENTS + 1) :]
- if not segments:
- return []
+ # To cap the number of complete segments at NUM_PLAYLIST_SEGMENTS,
+ # remove the first segment if the last segment is actually complete
+ if segments[-1].complete:
+ segments = segments[-NUM_PLAYLIST_SEGMENTS:]
+ first_segment = segments[0]
playlist = [
- f"#EXT-X-MEDIA-SEQUENCE:{segments[0].sequence}",
- f"#EXT-X-DISCONTINUITY-SEQUENCE:{segments[0].stream_id}",
+ "#EXTM3U",
+ "#EXT-X-VERSION:6",
+ "#EXT-X-INDEPENDENT-SEGMENTS",
+ '#EXT-X-MAP:URI="init.mp4"',
+ f"#EXT-X-TARGETDURATION:{track.target_duration:.0f}",
+ f"#EXT-X-MEDIA-SEQUENCE:{first_segment.sequence}",
+ f"#EXT-X-DISCONTINUITY-SEQUENCE:{first_segment.stream_id}",
+ "#EXT-X-PROGRAM-DATE-TIME:"
+ + first_segment.start_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
+ + "Z",
+ # Since our window doesn't have many segments, we don't want to start
+ # at the beginning or we risk a behind live window exception in Exoplayer.
+ # EXT-X-START is not supposed to be within 3 target durations of the end,
+ # but a value as low as 1.5 doesn't seem to hurt.
+ # A value below 3 may not be as useful for hls.js as many hls.js clients
+ # don't autoplay. Also, hls.js uses the player parameter liveSyncDuration
+ # which seems to take precedence for setting target delay. Yet it also
+ # doesn't seem to hurt, so we can stick with it for now.
+ f"#EXT-X-START:TIME-OFFSET=-{EXT_X_START * track.target_duration:.3f}",
]
- last_stream_id = segments[0].stream_id
+ last_stream_id = first_segment.stream_id
+ # Add playlist sections
for segment in segments:
- if last_stream_id != segment.stream_id:
- playlist.append("#EXT-X-DISCONTINUITY")
- playlist.extend(
- [
- f"#EXTINF:{float(segment.duration):.04f},",
- f"./segment/{segment.sequence}.m4s",
- ]
- )
- last_stream_id = segment.stream_id
+ # Skip last segment if it is not complete
+ if segment.complete:
+ if last_stream_id != segment.stream_id:
+ playlist.extend(
+ [
+ "#EXT-X-DISCONTINUITY",
+ "#EXT-X-PROGRAM-DATE-TIME:"
+ + segment.start_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
+ + "Z",
+ ]
+ )
+ playlist.extend(
+ [
+ f"#EXTINF:{segment.duration:.3f},",
+ f"./segment/{segment.sequence}.m4s",
+ ]
+ )
+ last_stream_id = segment.stream_id
- return playlist
+ return "\n".join(playlist) + "\n"
- def render(self, track):
- """Render M3U8 file."""
- lines = ["#EXTM3U"] + self.render_preamble(track) + self.render_playlist(track)
- return "\n".join(lines) + "\n"
-
- async def handle(self, request, stream, sequence):
+ async def handle(
+ self, request: web.Request, stream: Stream, sequence: str
+ ) -> web.Response:
"""Return m3u8 playlist."""
- track = stream.add_provider("hls")
+ track = stream.add_provider(HLS_PROVIDER)
stream.start()
- # Wait for a segment to be ready
- if not track.segments and not await track.recv():
+ # Make sure at least two segments are ready (last one may not be complete)
+ if not track.sequences and not await track.recv():
return web.HTTPNotFound()
- headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]}
- return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
+ if len(track.sequences) == 1 and not await track.recv():
+ return web.HTTPNotFound()
+ headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]}
+ response = web.Response(
+ body=self.render(track).encode("utf-8"), headers=headers
+ )
+ response.enable_compression(web.ContentCoding.gzip)
+ return response
class HlsInitView(StreamView):
@@ -120,14 +166,16 @@ class HlsInitView(StreamView):
name = "api:stream:hls:init"
cors_allowed = True
- async def handle(self, request, stream, sequence):
+ async def handle(
+ self, request: web.Request, stream: Stream, sequence: str
+ ) -> web.Response:
"""Return init.mp4."""
- track = stream.add_provider("hls")
- segments = track.get_segments()
- if not segments:
+ track = stream.add_provider(HLS_PROVIDER)
+ if not (segments := track.get_segments()):
return web.HTTPNotFound()
- headers = {"Content-Type": "video/mp4"}
- return web.Response(body=segments[0].init, headers=headers)
+ return web.Response(
+ body=segments[0].init, headers={"Content-Type": "video/mp4"}
+ )
class HlsSegmentView(StreamView):
@@ -137,20 +185,22 @@ class HlsSegmentView(StreamView):
name = "api:stream:hls:segment"
cors_allowed = True
- async def handle(self, request, stream, sequence):
+ async def handle(
+ self, request: web.Request, stream: Stream, sequence: str
+ ) -> web.Response:
"""Return fmp4 segment."""
- track = stream.add_provider("hls")
- segment = track.get_segment(int(sequence))
- if not segment:
+ track = stream.add_provider(HLS_PROVIDER)
+ track.idle_timer.awake()
+ if not (segment := track.get_segment(int(sequence))):
return web.HTTPNotFound()
headers = {"Content-Type": "video/iso.segment"}
return web.Response(
- body=segment.moof_data,
+ body=segment.get_bytes_without_init(),
headers=headers,
)
-@PROVIDERS.register("hls")
+@PROVIDERS.register(HLS_PROVIDER)
class HlsStreamOutput(StreamOutput):
"""Represents HLS Output formats."""
@@ -161,4 +211,4 @@ class HlsStreamOutput(StreamOutput):
@property
def name(self) -> str:
"""Return provider name."""
- return "hls"
+ return HLS_PROVIDER
diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py
index 7d849375ece..99276d9763c 100644
--- a/homeassistant/components/stream/recorder.py
+++ b/homeassistant/components/stream/recorder.py
@@ -12,18 +12,22 @@ from av.container import OutputContainer
from homeassistant.core import HomeAssistant, callback
-from .const import RECORDER_CONTAINER_FORMAT, SEGMENT_CONTAINER_FORMAT
+from .const import (
+ RECORDER_CONTAINER_FORMAT,
+ RECORDER_PROVIDER,
+ SEGMENT_CONTAINER_FORMAT,
+)
from .core import PROVIDERS, IdleTimer, Segment, StreamOutput
_LOGGER = logging.getLogger(__name__)
@callback
-def async_setup_recorder(hass):
+def async_setup_recorder(hass: HomeAssistant) -> None:
"""Only here so Provider Registry works."""
-def recorder_save_worker(file_out: str, segments: deque[Segment]):
+def recorder_save_worker(file_out: str, segments: deque[Segment]) -> None:
"""Handle saving stream."""
if not segments:
@@ -53,7 +57,7 @@ def recorder_save_worker(file_out: str, segments: deque[Segment]):
# Open segment
source = av.open(
- BytesIO(segment.init + segment.moof_data),
+ BytesIO(segment.init + segment.get_bytes_without_init()),
"r",
format=SEGMENT_CONTAINER_FORMAT,
)
@@ -110,25 +114,25 @@ def recorder_save_worker(file_out: str, segments: deque[Segment]):
output.close()
-@PROVIDERS.register("recorder")
+@PROVIDERS.register(RECORDER_PROVIDER)
class RecorderOutput(StreamOutput):
"""Represents HLS Output formats."""
def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None:
"""Initialize recorder output."""
super().__init__(hass, idle_timer)
- self.video_path = None
+ self.video_path: str
@property
def name(self) -> str:
"""Return provider name."""
- return "recorder"
+ return RECORDER_PROVIDER
def prepend(self, segments: list[Segment]) -> None:
"""Prepend segments to existing list."""
self._segments.extendleft(reversed(segments))
- def cleanup(self):
+ def cleanup(self) -> None:
"""Write recording and clean up."""
_LOGGER.debug("Starting recorder worker thread")
thread = threading.Thread(
diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py
index cb6d6a6a017..04be79e668e 100644
--- a/homeassistant/components/stream/worker.py
+++ b/homeassistant/components/stream/worker.py
@@ -2,9 +2,11 @@
from __future__ import annotations
from collections import deque
+from collections.abc import Iterator, Mapping
from io import BytesIO
import logging
-from typing import cast
+from threading import Event
+from typing import Any, Callable, cast
import av
@@ -16,10 +18,10 @@ from .const import (
MIN_SEGMENT_DURATION,
PACKETS_TO_WAIT_FOR_AUDIO,
SEGMENT_CONTAINER_FORMAT,
- STREAM_TIMEOUT,
+ SOURCE_TIMEOUT,
+ TARGET_PART_DURATION,
)
-from .core import Segment, StreamOutput
-from .fmp4utils import get_init_and_moof_data
+from .core import Part, Segment, StreamOutput
_LOGGER = logging.getLogger(__name__)
@@ -27,20 +29,29 @@ _LOGGER = logging.getLogger(__name__)
class SegmentBuffer:
"""Buffer for writing a sequence of packets to the output as a segment."""
- def __init__(self, outputs_callback) -> None:
+ def __init__(
+ self, outputs_callback: Callable[[], Mapping[str, StreamOutput]]
+ ) -> None:
"""Initialize SegmentBuffer."""
- self._stream_id = 0
- self._outputs_callback = outputs_callback
- self._outputs: list[StreamOutput] = []
- self._sequence = 0
- self._segment_start_pts = None
+ self._stream_id: int = 0
+ self._outputs_callback: Callable[
+ [], Mapping[str, StreamOutput]
+ ] = outputs_callback
+ # sequence gets incremented before the first segment so the first segment
+ # has a sequence number of 0.
+ self._sequence = -1
+ self._segment_start_dts: int = cast(int, None)
self._memory_file: BytesIO = cast(BytesIO, None)
self._av_output: av.container.OutputContainer = None
self._input_video_stream: av.video.VideoStream = None
- self._input_audio_stream = None # av.audio.AudioStream | None
+ self._input_audio_stream: Any | None = None # av.audio.AudioStream | None
self._output_video_stream: av.video.VideoStream = None
- self._output_audio_stream = None # av.audio.AudioStream | None
- self._segment: Segment = cast(Segment, None)
+ self._output_audio_stream: Any | None = None # av.audio.AudioStream | None
+ self._segment: Segment | None = None
+ # the following 3 member variables are used for Part formation
+ self._memory_file_pos: int = cast(int, None)
+ self._part_start_dts: int = cast(int, None)
+ self._part_has_keyframe = False
@staticmethod
def make_new_av(
@@ -54,33 +65,38 @@ class SegmentBuffer:
container_options={
# Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970
# "cmaf" flag replaces several of the movflags used, but too recent to use for now
- "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer",
- "avoid_negative_ts": "disabled",
+ "movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer",
+ # Sometimes the first segment begins with negative timestamps, and this setting just
+ # adjusts the timestamps in the output from that segment to start from 0. Helps from
+ # having to make some adjustments in test_durations
+ "avoid_negative_ts": "make_non_negative",
"fragment_index": str(sequence + 1),
"video_track_timescale": str(int(1 / input_vstream.time_base)),
+ # Create a fragments every TARGET_PART_DURATION. The data from each fragment is stored in
+ # a "Part" that can be combined with the data from all the other "Part"s, plus an init
+ # section, to reconstitute the data in a "Segment".
+ "frag_duration": str(int(TARGET_PART_DURATION * 1e6)),
},
)
def set_streams(
self,
video_stream: av.video.VideoStream,
- audio_stream,
+ audio_stream: Any,
# no type hint for audio_stream until https://github.com/PyAV-Org/PyAV/pull/775 is merged
) -> None:
"""Initialize output buffer with streams from container."""
self._input_video_stream = video_stream
self._input_audio_stream = audio_stream
- def reset(self, video_pts):
+ def reset(self, video_dts: int) -> None:
"""Initialize a new stream segment."""
# Keep track of the number of segments we've processed
self._sequence += 1
- self._segment_start_pts = video_pts
-
- # Fetch the latest StreamOutputs, which may have changed since the
- # worker started.
- self._outputs = self._outputs_callback().values()
+ self._segment_start_dts = video_dts
+ self._segment = None
self._memory_file = BytesIO()
+ self._memory_file_pos = 0
self._av_output = self.make_new_av(
memory_file=self._memory_file,
sequence=self._sequence,
@@ -96,58 +112,105 @@ class SegmentBuffer:
template=self._input_audio_stream
)
- def mux_packet(self, packet):
+ def mux_packet(self, packet: av.Packet) -> None:
"""Mux a packet to the appropriate output stream."""
# Check for end of segment
- if packet.stream == self._input_video_stream and packet.is_keyframe:
- duration = (packet.pts - self._segment_start_pts) * packet.time_base
- if duration >= MIN_SEGMENT_DURATION:
- # Save segment to outputs
- self.flush(duration)
-
- # Reinitialize
- self.reset(packet.pts)
-
- # Mux the packet
if packet.stream == self._input_video_stream:
+
+ if (
+ packet.is_keyframe
+ and (packet.dts - self._segment_start_dts) * packet.time_base
+ >= MIN_SEGMENT_DURATION
+ ):
+ # Flush segment (also flushes the stub part segment)
+ self.flush(packet, last_part=True)
+ # Reinitialize
+ self.reset(packet.dts)
+
+ # Mux the packet
packet.stream = self._output_video_stream
self._av_output.mux(packet)
+ self.check_flush_part(packet)
+ self._part_has_keyframe |= packet.is_keyframe
+
elif packet.stream == self._input_audio_stream:
packet.stream = self._output_audio_stream
self._av_output.mux(packet)
- def flush(self, duration):
- """Create a segment from the buffered packets and write to output."""
- self._av_output.close()
- segment = Segment(
- self._sequence,
- *get_init_and_moof_data(self._memory_file.getbuffer()),
- duration,
- self._stream_id,
- )
- self._memory_file.close()
- for stream_output in self._outputs:
- stream_output.put(segment)
+ def check_flush_part(self, packet: av.Packet) -> None:
+ """Check for and mark a part segment boundary and record its duration."""
+ if self._memory_file_pos == self._memory_file.tell():
+ return
+ if self._segment is None:
+ # We have our first non-zero byte position. This means the init has just
+ # been written. Create a Segment and put it to the queue of each output.
+ self._segment = Segment(
+ sequence=self._sequence,
+ stream_id=self._stream_id,
+ init=self._memory_file.getvalue(),
+ )
+ self._memory_file_pos = self._memory_file.tell()
+ self._part_start_dts = self._segment_start_dts
+ # Fetch the latest StreamOutputs, which may have changed since the
+ # worker started.
+ for stream_output in self._outputs_callback().values():
+ stream_output.put(self._segment)
+ else: # These are the ends of the part segments
+ self.flush(packet, last_part=False)
- def discontinuity(self):
+ def flush(self, packet: av.Packet, last_part: bool) -> None:
+ """Output a part from the most recent bytes in the memory_file.
+
+ If last_part is True, also close the segment, give it a duration,
+ and clean up the av_output and memory_file.
+ """
+ if last_part:
+ # Closing the av_output will write the remaining buffered data to the
+ # memory_file as a new moof/mdat.
+ self._av_output.close()
+ assert self._segment
+ self._memory_file.seek(self._memory_file_pos)
+ self._segment.parts.append(
+ Part(
+ duration=float((packet.dts - self._part_start_dts) * packet.time_base),
+ has_keyframe=self._part_has_keyframe,
+ data=self._memory_file.read(),
+ )
+ )
+ if last_part:
+ self._segment.duration = float(
+ (packet.dts - self._segment_start_dts) * packet.time_base
+ )
+ self._memory_file.close() # We don't need the BytesIO object anymore
+ else:
+ self._memory_file_pos = self._memory_file.tell()
+ self._part_start_dts = packet.dts
+ self._part_has_keyframe = False
+
+ def discontinuity(self) -> None:
"""Mark the stream as having been restarted."""
# Preserving sequence and stream_id here keep the HLS playlist logic
# simple to check for discontinuity at output time, and to determine
# the discontinuity sequence number.
self._stream_id += 1
- def close(self):
+ def close(self) -> None:
"""Close stream buffer."""
self._av_output.close()
self._memory_file.close()
-def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901
+def stream_worker( # noqa: C901
+ source: str,
+ options: dict[str, str],
+ segment_buffer: SegmentBuffer,
+ quit_event: Event,
+) -> None:
"""Handle consuming streams."""
try:
- container = av.open(source, options=options, timeout=STREAM_TIMEOUT)
+ container = av.open(source, options=options, timeout=SOURCE_TIMEOUT)
except av.AVError:
_LOGGER.error("Error opening stream %s", redact_credentials(str(source)))
return
@@ -170,32 +233,32 @@ def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901
audio_stream = None
# Iterator for demuxing
- container_packets = None
+ container_packets: Iterator[av.Packet]
# The decoder timestamps of the latest packet in each stream we processed
last_dts = {video_stream: float("-inf"), audio_stream: float("-inf")}
# Keep track of consecutive packets without a dts to detect end of stream.
missing_dts = 0
- # The video pts at the beginning of the segment
- segment_start_pts = None
+ # The video dts at the beginning of the segment
+ segment_start_dts: int | None = None
# Because of problems 1 and 2 below, we need to store the first few packets and replay them
- initial_packets = deque()
+ initial_packets: deque[av.Packet] = deque()
# Have to work around two problems with RTSP feeds in ffmpeg
# 1 - first frame has bad pts/dts https://trac.ffmpeg.org/ticket/5018
# 2 - seeking can be problematic https://trac.ffmpeg.org/ticket/7815
- def peek_first_pts():
+ def peek_first_dts() -> bool:
"""Initialize by peeking into the first few packets of the stream.
Deal with problem #1 above (bad first packet pts/dts) by recalculating using pts/dts from second packet.
- Also load the first video keyframe pts into segment_start_pts and check if the audio stream really exists.
+ Also load the first video keyframe dts into segment_start_dts and check if the audio stream really exists.
"""
- nonlocal segment_start_pts, audio_stream, container_packets
+ nonlocal segment_start_dts, audio_stream, container_packets
missing_dts = 0
found_audio = False
try:
container_packets = container.demux((video_stream, audio_stream))
- first_packet = None
+ first_packet: av.Packet | None = None
# Get to first video keyframe
while first_packet is None:
packet = next(container_packets)
@@ -213,8 +276,8 @@ def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901
elif packet.is_keyframe: # video_keyframe
first_packet = packet
initial_packets.append(packet)
- # Get first_pts from subsequent frame to first keyframe
- while segment_start_pts is None or (
+ # Get first_dts from subsequent frame to first keyframe
+ while segment_start_dts is None or (
audio_stream
and not found_audio
and len(initial_packets) < PACKETS_TO_WAIT_FOR_AUDIO
@@ -242,17 +305,15 @@ def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901
continue
found_audio = True
elif (
- segment_start_pts is None
- ): # This is the second video frame to calculate first_pts from
- segment_start_pts = packet.dts - packet.duration
- first_packet.pts = segment_start_pts
- first_packet.dts = segment_start_pts
+ segment_start_dts is None
+ ): # This is the second video frame to calculate first_dts from
+ segment_start_dts = packet.dts - packet.duration
+ first_packet.pts = first_packet.dts = segment_start_dts
initial_packets.append(packet)
if audio_stream and not found_audio:
_LOGGER.warning(
"Audio stream not found"
) # Some streams declare an audio stream and never send any packets
- audio_stream = None
except (av.AVError, StopIteration) as ex:
_LOGGER.error(
@@ -261,12 +322,13 @@ def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901
return False
return True
- if not peek_first_pts():
+ if not peek_first_dts():
container.close()
return
segment_buffer.set_streams(video_stream, audio_stream)
- segment_buffer.reset(segment_start_pts)
+ assert isinstance(segment_start_dts, int)
+ segment_buffer.reset(segment_start_dts)
while not quit_event.is_set():
try:
diff --git a/homeassistant/components/subaru/translations/de.json b/homeassistant/components/subaru/translations/de.json
index dd2fb797dbc..ac953654565 100644
--- a/homeassistant/components/subaru/translations/de.json
+++ b/homeassistant/components/subaru/translations/de.json
@@ -15,7 +15,8 @@
"data": {
"pin": "PIN"
},
- "description": "Bitte gib deinen MySubaru-PIN ein\nHINWEIS: Alle Fahrzeuge im Konto m\u00fcssen dieselbe PIN haben"
+ "description": "Bitte gib deinen MySubaru-PIN ein\nHINWEIS: Alle Fahrzeuge im Konto m\u00fcssen dieselbe PIN haben",
+ "title": "Subaru Starlink Konfiguration"
},
"user": {
"data": {
diff --git a/homeassistant/components/subaru/translations/he.json b/homeassistant/components/subaru/translations/he.json
new file mode 100644
index 00000000000..51413724a5f
--- /dev/null
+++ b/homeassistant/components/subaru/translations/he.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "\u05e7\u05d5\u05d3 PIN"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sun/trigger.py b/homeassistant/components/sun/trigger.py
index d2b6f6de560..b612934bfad 100644
--- a/homeassistant/components/sun/trigger.py
+++ b/homeassistant/components/sun/trigger.py
@@ -15,7 +15,7 @@ from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
# mypy: allow-untyped-defs, no-check-untyped-defs
-TRIGGER_SCHEMA = vol.Schema(
+TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "sun",
vol.Required(CONF_EVENT): cv.sun_event,
@@ -26,7 +26,7 @@ TRIGGER_SCHEMA = vol.Schema(
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for events based on configuration."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
event = config.get(CONF_EVENT)
offset = config.get(CONF_OFFSET)
description = event
@@ -41,11 +41,11 @@ async def async_attach_trigger(hass, config, action, automation_info):
job,
{
"trigger": {
+ **trigger_data,
"platform": "sun",
"event": event,
"offset": offset,
"description": description,
- "id": trigger_id,
}
},
)
diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py
index 3283b4c97c9..8f0c2311518 100644
--- a/homeassistant/components/surepetcare/__init__.py
+++ b/homeassistant/components/surepetcare/__init__.py
@@ -90,12 +90,10 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
async_track_time_interval(hass, spc.async_update, SCAN_INTERVAL)
# load platforms
- hass.async_create_task(
- hass.helpers.discovery.async_load_platform("binary_sensor", DOMAIN, {}, config)
- )
- hass.async_create_task(
- hass.helpers.discovery.async_load_platform("sensor", DOMAIN, {}, config)
- )
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.helpers.discovery.async_load_platform(platform, DOMAIN, {}, config)
+ )
async def handle_set_lock_state(call):
"""Call when setting the lock state."""
@@ -150,6 +148,7 @@ class SurePetcareAPI:
self.states = await self.surepy.get_entities()
except SurePetcareError as error:
_LOGGER.error("Unable to fetch data: %s", error)
+ return
async_dispatcher_send(self.hass, TOPIC_UPDATE)
diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py
index fd75264ee27..ca7b7378127 100644
--- a/homeassistant/components/surepetcare/binary_sensor.py
+++ b/homeassistant/components/surepetcare/binary_sensor.py
@@ -1,11 +1,11 @@
"""Support for Sure PetCare Flaps/Pets binary sensors."""
from __future__ import annotations
+from abc import abstractmethod
import logging
-from typing import Any
from surepy.entities import SurepyEntity
-from surepy.enums import EntityType, Location, SureEnum
+from surepy.enums import EntityType, Location
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY,
@@ -41,9 +41,7 @@ async def async_setup_platform(
EntityType.FEEDER,
EntityType.FELAQUA,
]:
- entities.append(
- DeviceConnectivity(surepy_entity.id, surepy_entity.type, spc)
- )
+ entities.append(DeviceConnectivity(surepy_entity.id, spc))
if surepy_entity.type == EntityType.PET:
entities.append(Pet(surepy_entity.id, spc))
@@ -56,57 +54,41 @@ async def async_setup_platform(
class SurePetcareBinarySensor(BinarySensorEntity):
"""A binary sensor implementation for Sure Petcare Entities."""
+ _attr_should_poll = False
+
def __init__(
self,
_id: int,
spc: SurePetcareAPI,
device_class: str,
- sure_type: EntityType,
) -> None:
"""Initialize a Sure Petcare binary sensor."""
self._id = _id
- self._device_class = device_class
-
self._spc: SurePetcareAPI = spc
- self._surepy_entity: SurepyEntity = self._spc.states[self._id]
- self._state: SureEnum | dict[str, Any] = None
+ surepy_entity: SurepyEntity = self._spc.states[self._id]
# cover special case where a device has no name set
- if self._surepy_entity.name:
- name = self._surepy_entity.name
+ if surepy_entity.name:
+ name = surepy_entity.name
else:
- name = f"Unnamed {self._surepy_entity.type.name.capitalize()}"
+ name = f"Unnamed {surepy_entity.type.name.capitalize()}"
- self._name = f"{self._surepy_entity.type.name.capitalize()} {name.capitalize()}"
+ self._name = f"{surepy_entity.type.name.capitalize()} {name.capitalize()}"
- @property
- def should_poll(self) -> bool:
- """Return if the entity should use default polling."""
- return False
+ self._attr_device_class = device_class
+ self._attr_unique_id = f"{surepy_entity.household_id}-{self._id}"
@property
def name(self) -> str:
"""Return the name of the device if any."""
return self._name
- @property
- def device_class(self) -> str:
- """Return the device class."""
- return None if not self._device_class else self._device_class
-
- @property
- def unique_id(self) -> str:
- """Return an unique ID."""
- return f"{self._surepy_entity.household_id}-{self._id}"
-
+ @abstractmethod
@callback
def _async_update(self) -> None:
"""Get the latest data and update the state."""
- self._surepy_entity = self._spc.states[self._id]
- self._state = self._surepy_entity.raw_data()["status"]
- _LOGGER.debug("%s -> self._state: %s", self._name, self._state)
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@@ -121,31 +103,25 @@ class Hub(SurePetcareBinarySensor):
def __init__(self, _id: int, spc: SurePetcareAPI) -> None:
"""Initialize a Sure Petcare Hub."""
- super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, EntityType.HUB)
+ super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY)
- @property
- def available(self) -> bool:
- """Return true if entity is available."""
- return bool(self._state["online"])
-
- @property
- def is_on(self) -> bool:
- """Return true if entity is online."""
- return self.available
-
- @property
- def extra_state_attributes(self) -> dict[str, Any] | None:
- """Return the state attributes of the device."""
- attributes = None
- if self._surepy_entity.raw_data():
- attributes = {
- "led_mode": int(self._surepy_entity.raw_data()["status"]["led_mode"]),
+ @callback
+ def _async_update(self) -> None:
+ """Get the latest data and update the state."""
+ surepy_entity = self._spc.states[self._id]
+ state = surepy_entity.raw_data()["status"]
+ self._attr_is_on = self._attr_available = bool(state["online"])
+ if surepy_entity.raw_data():
+ self._attr_extra_state_attributes = {
+ "led_mode": int(surepy_entity.raw_data()["status"]["led_mode"]),
"pairing_mode": bool(
- self._surepy_entity.raw_data()["status"]["pairing_mode"]
+ surepy_entity.raw_data()["status"]["pairing_mode"]
),
}
-
- return attributes
+ else:
+ self._attr_extra_state_attributes = None
+ _LOGGER.debug("%s -> state: %s", self._name, state)
+ self.async_write_ha_state()
class Pet(SurePetcareBinarySensor):
@@ -153,31 +129,26 @@ class Pet(SurePetcareBinarySensor):
def __init__(self, _id: int, spc: SurePetcareAPI) -> None:
"""Initialize a Sure Petcare Pet."""
- super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, EntityType.PET)
-
- @property
- def is_on(self) -> bool:
- """Return true if entity is at home."""
- try:
- return bool(Location(self._state.where) == Location.INSIDE)
- except (KeyError, TypeError):
- return False
-
- @property
- def extra_state_attributes(self) -> dict[str, Any] | None:
- """Return the state attributes of the device."""
- attributes = None
- if self._state:
- attributes = {"since": self._state.since, "where": self._state.where}
-
- return attributes
+ super().__init__(_id, spc, DEVICE_CLASS_PRESENCE)
@callback
def _async_update(self) -> None:
"""Get the latest data and update the state."""
- self._surepy_entity = self._spc.states[self._id]
- self._state = self._surepy_entity.location
- _LOGGER.debug("%s -> self._state: %s", self._name, self._state)
+ surepy_entity = self._spc.states[self._id]
+ state = surepy_entity.location
+ try:
+ self._attr_is_on = bool(Location(state.where) == Location.INSIDE)
+ except (KeyError, TypeError):
+ self._attr_is_on = False
+ if state:
+ self._attr_extra_state_attributes = {
+ "since": state.since,
+ "where": state.where,
+ }
+ else:
+ self._attr_extra_state_attributes = None
+ _LOGGER.debug("%s -> state: %s", self._name, state)
+ self.async_write_ha_state()
class DeviceConnectivity(SurePetcareBinarySensor):
@@ -186,40 +157,31 @@ class DeviceConnectivity(SurePetcareBinarySensor):
def __init__(
self,
_id: int,
- sure_type: EntityType,
spc: SurePetcareAPI,
) -> None:
"""Initialize a Sure Petcare Device."""
- super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, sure_type)
+ super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY)
+ self._attr_unique_id = (
+ f"{self._spc.states[self._id].household_id}-{self._id}-connectivity"
+ )
@property
def name(self) -> str:
"""Return the name of the device if any."""
return f"{self._name}_connectivity"
- @property
- def unique_id(self) -> str:
- """Return an unique ID."""
- return f"{self._surepy_entity.household_id}-{self._id}-connectivity"
-
- @property
- def available(self) -> bool:
- """Return true if entity is available."""
- return bool(self._state)
-
- @property
- def is_on(self) -> bool:
- """Return true if entity is online."""
- return self.available
-
- @property
- def extra_state_attributes(self) -> dict[str, Any] | None:
- """Return the state attributes of the device."""
- attributes = None
- if self._state:
- attributes = {
- "device_rssi": f'{self._state["signal"]["device_rssi"]:.2f}',
- "hub_rssi": f'{self._state["signal"]["hub_rssi"]:.2f}',
+ @callback
+ def _async_update(self) -> None:
+ """Get the latest data and update the state."""
+ surepy_entity = self._spc.states[self._id]
+ state = surepy_entity.raw_data()["status"]
+ self._attr_is_on = self._attr_available = bool(self.state)
+ if state:
+ self._attr_extra_state_attributes = {
+ "device_rssi": f'{state["signal"]["device_rssi"]:.2f}',
+ "hub_rssi": f'{state["signal"]["hub_rssi"]:.2f}',
}
-
- return attributes
+ else:
+ self._attr_extra_state_attributes = None
+ _LOGGER.debug("%s -> state: %s", self._name, state)
+ self.async_write_ha_state()
diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json
index 231ede6474f..1f0804e0581 100644
--- a/homeassistant/components/surepetcare/manifest.json
+++ b/homeassistant/components/surepetcare/manifest.json
@@ -2,7 +2,7 @@
"domain": "surepetcare",
"name": "Sure Petcare",
"documentation": "https://www.home-assistant.io/integrations/surepetcare",
- "codeowners": ["@benleb"],
+ "codeowners": ["@benleb", "@danielhiversen"],
"requirements": ["surepy==0.6.0"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py
index cfdb25fd412..fbc8222f292 100644
--- a/homeassistant/components/surepetcare/sensor.py
+++ b/homeassistant/components/surepetcare/sensor.py
@@ -2,7 +2,6 @@
from __future__ import annotations
import logging
-from typing import Any
from surepy.entities import SurepyEntity
from surepy.enums import EntityType
@@ -46,8 +45,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(entities)
-class SurePetcareSensor(SensorEntity):
- """A binary sensor implementation for Sure Petcare Entities."""
+class SureBattery(SensorEntity):
+ """A sensor implementation for Sure Petcare Entities."""
+
+ _attr_should_poll = False
def __init__(self, _id: int, spc: SurePetcareAPI) -> None:
"""Initialize a Sure Petcare sensor."""
@@ -55,29 +56,41 @@ class SurePetcareSensor(SensorEntity):
self._id = _id
self._spc: SurePetcareAPI = spc
- self._surepy_entity: SurepyEntity = self._spc.states[_id]
- self._state: dict[str, Any] = {}
- self._name = (
- f"{self._surepy_entity.type.name.capitalize()} "
- f"{self._surepy_entity.name.capitalize()}"
+ surepy_entity: SurepyEntity = self._spc.states[_id]
+
+ self._attr_device_class = DEVICE_CLASS_BATTERY
+ self._attr_name = f"{surepy_entity.type.name.capitalize()} {surepy_entity.name.capitalize()} Battery Level"
+ self._attr_unit_of_measurement = PERCENTAGE
+ self._attr_unique_id = (
+ f"{surepy_entity.household_id}-{surepy_entity.id}-battery"
)
- @property
- def available(self) -> bool:
- """Return true if entity is available."""
- return bool(self._state)
-
- @property
- def should_poll(self) -> bool:
- """Return true."""
- return False
-
@callback
def _async_update(self) -> None:
"""Get the latest data and update the state."""
- self._surepy_entity = self._spc.states[self._id]
- self._state = self._surepy_entity.raw_data()["status"]
- _LOGGER.debug("%s -> self._state: %s", self._name, self._state)
+ surepy_entity = self._spc.states[self._id]
+ state = surepy_entity.raw_data()["status"]
+
+ self._attr_available = bool(state)
+ try:
+ per_battery_voltage = state["battery"] / 4
+ voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW
+ self._attr_state = min(
+ int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100), 100
+ )
+ except (KeyError, TypeError):
+ self._attr_state = None
+
+ if state:
+ voltage_per_battery = float(state["battery"]) / 4
+ self._attr_extra_state_attributes = {
+ ATTR_VOLTAGE: f"{float(state['battery']):.2f}",
+ f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}",
+ }
+ else:
+ self._attr_extra_state_attributes = None
+ self.async_write_ha_state()
+ _LOGGER.debug("%s -> state: %s", self.name, state)
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@@ -85,53 +98,3 @@ class SurePetcareSensor(SensorEntity):
async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._async_update)
)
self._async_update()
-
-
-class SureBattery(SurePetcareSensor):
- """Sure Petcare Flap."""
-
- @property
- def name(self) -> str:
- """Return the name of the device if any."""
- return f"{self._name} Battery Level"
-
- @property
- def state(self) -> int | None:
- """Return battery level in percent."""
- battery_percent: int | None
- try:
- per_battery_voltage = self._state["battery"] / 4
- voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW
- battery_percent = min(int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100), 100)
- except (KeyError, TypeError):
- battery_percent = None
-
- return battery_percent
-
- @property
- def unique_id(self) -> str:
- """Return an unique ID."""
- return f"{self._surepy_entity.household_id}-{self._surepy_entity.id}-battery"
-
- @property
- def device_class(self) -> str:
- """Return the device class."""
- return DEVICE_CLASS_BATTERY
-
- @property
- def extra_state_attributes(self) -> dict[str, Any] | None:
- """Return state attributes."""
- attributes = None
- if self._state:
- voltage_per_battery = float(self._state["battery"]) / 4
- attributes = {
- ATTR_VOLTAGE: f"{float(self._state['battery']):.2f}",
- f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}",
- }
-
- return attributes
-
- @property
- def unit_of_measurement(self) -> str:
- """Return the unit of measurement."""
- return PERCENTAGE
diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py
index c585fdc22d3..1ef48fed620 100644
--- a/homeassistant/components/switch/__init__.py
+++ b/homeassistant/components/switch/__init__.py
@@ -1,26 +1,29 @@
"""Component to interface with switches that can be controlled remotely."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import final
+from typing import Any, final
import voluptuous as vol
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_ON,
)
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
-# mypy: allow-untyped-defs, no-check-untyped-defs
-
DOMAIN = "switch"
SCAN_INTERVAL = timedelta(seconds=30)
@@ -47,7 +50,7 @@ _LOGGER = logging.getLogger(__name__)
@bind_hass
-def is_on(hass, entity_id):
+def is_on(hass: HomeAssistant, entity_id: str) -> bool:
"""Return if the switch is on based on the statemachine.
Async friendly.
@@ -55,7 +58,7 @@ def is_on(hass, entity_id):
return hass.states.is_state(entity_id, STATE_ON)
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Track states and offer events for switches."""
component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
@@ -69,37 +72,37 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class SwitchEntity(ToggleEntity):
"""Base class for switch entities."""
+ _attr_current_power_w: float | None = None
+ _attr_today_energy_kwh: float | None = None
+
@property
- def current_power_w(self):
+ def current_power_w(self) -> float | None:
"""Return the current power usage in W."""
- return None
+ return self._attr_current_power_w
@property
- def today_energy_kwh(self):
+ def today_energy_kwh(self) -> float | None:
"""Return the today total energy usage in kWh."""
- return None
-
- @property
- def is_standby(self):
- """Return true if device is in standby."""
- return None
+ return self._attr_today_energy_kwh
@final
@property
- def state_attributes(self):
+ def state_attributes(self) -> dict[str, Any] | None:
"""Return the optional state attributes."""
data = {}
@@ -110,18 +113,13 @@ class SwitchEntity(ToggleEntity):
return data
- @property
- def device_class(self):
- """Return the class of this device, from component DEVICE_CLASSES."""
- return None
-
class SwitchDevice(SwitchEntity):
"""Representation of a switch (for backwards compatibility)."""
- def __init_subclass__(cls, **kwargs):
+ def __init_subclass__(cls, **kwargs: Any) -> None:
"""Print deprecation warning."""
- super().__init_subclass__(**kwargs)
+ super().__init_subclass__(**kwargs) # type: ignore[call-arg]
_LOGGER.warning(
"SwitchDevice is deprecated, modify %s to extend SwitchEntity",
cls.__name__,
diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py
index 5483ad88c2d..ef196220656 100644
--- a/homeassistant/components/switcher_kis/__init__.py
+++ b/homeassistant/components/switcher_kis/__init__.py
@@ -1,4 +1,4 @@
-"""Home Assistant Switcher Component."""
+"""The Switcher integration."""
from __future__ import annotations
from asyncio import QueueEmpty, TimeoutError as Asyncio_TimeoutError, wait_for
@@ -8,7 +8,6 @@ import logging
from aioswitcher.bridge import SwitcherV2Bridge
import voluptuous as vol
-from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
@@ -17,21 +16,16 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import EventType
+from .const import (
+ CONF_DEVICE_PASSWORD,
+ CONF_PHONE_ID,
+ DATA_DEVICE,
+ DOMAIN,
+ SIGNAL_SWITCHER_DEVICE_UPDATE,
+)
+
_LOGGER = logging.getLogger(__name__)
-DOMAIN = "switcher_kis"
-
-CONF_DEVICE_PASSWORD = "device_password"
-CONF_PHONE_ID = "phone_id"
-
-DATA_DEVICE = "device"
-
-SIGNAL_SWITCHER_DEVICE_UPDATE = "switcher_device_update"
-
-ATTR_AUTO_OFF_SET = "auto_off_set"
-ATTR_ELECTRIC_CURRENT = "electric_current"
-ATTR_REMAINING_TIME = "remaining_time"
-
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
@@ -70,7 +64,8 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
return False
hass.data[DOMAIN] = {DATA_DEVICE: device_data}
- hass.async_create_task(async_load_platform(hass, SWITCH_DOMAIN, DOMAIN, {}, config))
+ hass.async_create_task(async_load_platform(hass, "switch", DOMAIN, {}, config))
+ hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
@callback
def device_updates(timestamp: datetime | None) -> None:
diff --git a/homeassistant/components/switcher_kis/const.py b/homeassistant/components/switcher_kis/const.py
new file mode 100644
index 00000000000..acd6c070337
--- /dev/null
+++ b/homeassistant/components/switcher_kis/const.py
@@ -0,0 +1,20 @@
+"""Constants for the Switcher integration."""
+
+DOMAIN = "switcher_kis"
+
+CONF_DEVICE_PASSWORD = "device_password"
+CONF_PHONE_ID = "phone_id"
+
+DATA_DEVICE = "device"
+
+SIGNAL_SWITCHER_DEVICE_UPDATE = "switcher_device_update"
+
+ATTR_AUTO_OFF_SET = "auto_off_set"
+ATTR_ELECTRIC_CURRENT = "electric_current"
+ATTR_REMAINING_TIME = "remaining_time"
+
+CONF_AUTO_OFF = "auto_off"
+CONF_TIMER_MINUTES = "timer_minutes"
+
+SERVICE_SET_AUTO_OFF_NAME = "set_auto_off"
+SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer"
diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json
index 7344e2d05c0..84527954a2d 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"],
- "requirements": ["aioswitcher==1.2.1"],
+ "codeowners": ["@tomerfi","@thecode"],
+ "requirements": ["aioswitcher==1.2.3"],
"iot_class": "local_push"
}
diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py
new file mode 100644
index 00000000000..5b6b40a0e2d
--- /dev/null
+++ b/homeassistant/components/switcher_kis/sensor.py
@@ -0,0 +1,131 @@
+"""Switcher integration Sensor platform."""
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from aioswitcher.consts import WAITING_TEXT
+from aioswitcher.devices import SwitcherV2Device
+
+from homeassistant.components.sensor import (
+ DEVICE_CLASS_CURRENT,
+ DEVICE_CLASS_POWER,
+ STATE_CLASS_MEASUREMENT,
+ SensorEntity,
+)
+from homeassistant.const import ELECTRICAL_CURRENT_AMPERE, POWER_WATT
+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 DiscoveryInfoType, StateType
+
+from .const import DATA_DEVICE, DOMAIN, SIGNAL_SWITCHER_DEVICE_UPDATE
+
+
+@dataclass
+class AttributeDescription:
+ """Class to describe a sensor."""
+
+ name: str
+ icon: str | None = None
+ unit: str | None = None
+ device_class: str | None = None
+ state_class: str | None = None
+ default_enabled: bool = True
+ default_value: float | int | str | None = None
+
+
+POWER_SENSORS = {
+ "power_consumption": AttributeDescription(
+ name="Power Consumption",
+ unit=POWER_WATT,
+ device_class=DEVICE_CLASS_POWER,
+ state_class=STATE_CLASS_MEASUREMENT,
+ default_value=0,
+ ),
+ "electric_current": AttributeDescription(
+ name="Electric Current",
+ unit=ELECTRICAL_CURRENT_AMPERE,
+ device_class=DEVICE_CLASS_CURRENT,
+ state_class=STATE_CLASS_MEASUREMENT,
+ default_value=0.0,
+ ),
+}
+
+TIME_SENSORS = {
+ "remaining_time": AttributeDescription(
+ name="Remaining Time",
+ icon="mdi:av-timer",
+ default_value="00:00:00",
+ ),
+ "auto_off_set": AttributeDescription(
+ name="Auto Shutdown",
+ icon="mdi:progress-clock",
+ default_enabled=False,
+ default_value="00:00:00",
+ ),
+}
+
+SENSORS = {**POWER_SENSORS, **TIME_SENSORS}
+
+
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: dict,
+ async_add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType,
+) -> None:
+ """Set up Switcher sensor from config entry."""
+ device_data = hass.data[DOMAIN][DATA_DEVICE]
+
+ async_add_entities(
+ SwitcherSensorEntity(device_data, attribute, SENSORS[attribute])
+ for attribute in SENSORS
+ )
+
+
+class SwitcherSensorEntity(SensorEntity):
+ """Representation of a Switcher sensor entity."""
+
+ def __init__(
+ self,
+ device_data: SwitcherV2Device,
+ attribute: str,
+ description: AttributeDescription,
+ ) -> None:
+ """Initialize the entity."""
+ self._device_data = device_data
+ self.attribute = attribute
+ self.description = description
+
+ # Entity class attributes
+ self._attr_name = f"{self._device_data.name} {self.description.name}"
+ self._attr_icon = self.description.icon
+ self._attr_unit_of_measurement = self.description.unit
+ self._attr_device_class = self.description.device_class
+ self._attr_entity_registry_enabled_default = self.description.default_enabled
+ self._attr_should_poll = False
+
+ self._attr_unique_id = f"{self._device_data.device_id}-{self._device_data.mac_addr}-{self.attribute}"
+
+ @property
+ def state(self) -> StateType:
+ """Return value of sensor."""
+ value = getattr(self._device_data, self.attribute)
+ if value and value is not WAITING_TEXT:
+ return value
+
+ return self.description.default_value
+
+ async def async_added_to_hass(self) -> None:
+ """Run when entity about to be added to hass."""
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass, SIGNAL_SWITCHER_DEVICE_UPDATE, self.async_update_data
+ )
+ )
+
+ @callback
+ def async_update_data(self, device_data: SwitcherV2Device) -> None:
+ """Update the entity data."""
+ self._device_data = device_data
+ self.async_write_ha_state()
diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py
index 8f7332162a9..21ebcf54cc7 100644
--- a/homeassistant/components/switcher_kis/switch.py
+++ b/homeassistant/components/switcher_kis/switch.py
@@ -8,42 +8,30 @@ from aioswitcher.consts import (
COMMAND_ON,
STATE_OFF as SWITCHER_STATE_OFF,
STATE_ON as SWITCHER_STATE_ON,
- WAITING_TEXT,
)
from aioswitcher.devices import SwitcherV2Device
import voluptuous as vol
-from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchEntity
-from homeassistant.core import HomeAssistant, ServiceCall
+from homeassistant.components.switch import SwitchEntity
+from homeassistant.core import HomeAssistant, ServiceCall, 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 . import (
- ATTR_AUTO_OFF_SET,
- ATTR_ELECTRIC_CURRENT,
- ATTR_REMAINING_TIME,
+from .const import (
+ CONF_AUTO_OFF,
+ CONF_TIMER_MINUTES,
DATA_DEVICE,
DOMAIN,
+ SERVICE_SET_AUTO_OFF_NAME,
+ SERVICE_TURN_ON_WITH_TIMER_NAME,
SIGNAL_SWITCHER_DEVICE_UPDATE,
)
-CONF_AUTO_OFF = "auto_off"
-CONF_TIMER_MINUTES = "timer_minutes"
-
-DEVICE_PROPERTIES_TO_HA_ATTRIBUTES = {
- "power_consumption": ATTR_CURRENT_POWER_W,
- "electric_current": ATTR_ELECTRIC_CURRENT,
- "remaining_time": ATTR_REMAINING_TIME,
- "auto_off_set": ATTR_AUTO_OFF_SET,
-}
-
-SERVICE_SET_AUTO_OFF_NAME = "set_auto_off"
SERVICE_SET_AUTO_OFF_SCHEMA = {
vol.Required(CONF_AUTO_OFF): cv.time_period_str,
}
-SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer"
SERVICE_TURN_ON_WITH_TIMER_SCHEMA = {
vol.Required(CONF_TIMER_MINUTES): vol.All(
cv.positive_int, vol.Range(min=1, max=150)
@@ -134,23 +122,6 @@ class SwitcherControl(SwitchEntity):
"""Return True if entity is on."""
return self._state == SWITCHER_STATE_ON
- @property
- def current_power_w(self) -> int:
- """Return the current power usage in W."""
- return self._device_data.power_consumption
-
- @property
- def extra_state_attributes(self) -> dict:
- """Return the optional state attributes."""
- attribs = {}
-
- for prop, attr in DEVICE_PROPERTIES_TO_HA_ATTRIBUTES.items():
- value = getattr(self._device_data, prop)
- if value and value is not WAITING_TEXT:
- attribs[attr] = value
-
- return attribs
-
@property
def available(self) -> bool:
"""Return True if entity is available."""
@@ -164,15 +135,15 @@ class SwitcherControl(SwitchEntity):
)
)
- async def async_update_data(self, device_data: SwitcherV2Device) -> None:
+ @callback
+ def async_update_data(self, device_data: SwitcherV2Device) -> None:
"""Update the entity data."""
- if device_data:
- if self._self_initiated:
- self._self_initiated = False
- else:
- self._device_data = device_data
- self._state = self._device_data.state
- self.async_write_ha_state()
+ if self._self_initiated:
+ self._self_initiated = False
+ else:
+ self._device_data = device_data
+ self._state = self._device_data.state
+ self.async_write_ha_state()
async def async_turn_on(self, **kwargs: dict) -> None:
"""Turn the entity on."""
diff --git a/homeassistant/components/syncthing/translations/he.json b/homeassistant/components/syncthing/translations/he.json
new file mode 100644
index 00000000000..7e87dacd7e5
--- /dev/null
+++ b/homeassistant/components/syncthing/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df",
+ "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
+ }
+ }
+ }
+ },
+ "title": "\u05e1\u05d9\u05e0\u05db\u05e8\u05d5\u05df"
+}
\ No newline at end of file
diff --git a/homeassistant/components/syncthru/binary_sensor.py b/homeassistant/components/syncthru/binary_sensor.py
index 66bf76b31a5..7c4bd6fa8d1 100644
--- a/homeassistant/components/syncthru/binary_sensor.py
+++ b/homeassistant/components/syncthru/binary_sensor.py
@@ -75,16 +75,13 @@ class SyncThruBinarySensor(CoordinatorEntity, BinarySensorEntity):
class SyncThruOnlineSensor(SyncThruBinarySensor):
"""Implementation of a sensor that checks whether is turned on/online."""
+ _attr_device_class = DEVICE_CLASS_CONNECTIVITY
+
def __init__(self, syncthru, name):
"""Initialize the sensor."""
super().__init__(syncthru, name)
self._id_suffix = "_online"
- @property
- def device_class(self):
- """Class of the sensor."""
- return DEVICE_CLASS_CONNECTIVITY
-
@property
def is_on(self):
"""Set the state to whether the printer is online."""
@@ -94,16 +91,13 @@ class SyncThruOnlineSensor(SyncThruBinarySensor):
class SyncThruProblemSensor(SyncThruBinarySensor):
"""Implementation of a sensor that checks whether the printer works correctly."""
+ _attr_device_class = DEVICE_CLASS_PROBLEM
+
def __init__(self, syncthru, name):
"""Initialize the sensor."""
super().__init__(syncthru, name)
self._id_suffix = "_problem"
- @property
- def device_class(self):
- """Class of the sensor."""
- return DEVICE_CLASS_PROBLEM
-
@property
def is_on(self):
"""Set the state to whether there is a problem with the printer."""
diff --git a/homeassistant/components/syncthru/translations/de.json b/homeassistant/components/syncthru/translations/de.json
index 450d0466597..f7533630216 100644
--- a/homeassistant/components/syncthru/translations/de.json
+++ b/homeassistant/components/syncthru/translations/de.json
@@ -8,7 +8,7 @@
"syncthru_not_supported": "Ger\u00e4t unterst\u00fctzt kein SyncThru",
"unknown_state": "Druckerstatus unbekannt, \u00fcberpr\u00fcfe URL und Netzwerkverbindung"
},
- "flow_title": "Samsung SyncThru Drucker: {name}",
+ "flow_title": "{name}",
"step": {
"confirm": {
"data": {
diff --git a/homeassistant/components/syncthru/translations/he.json b/homeassistant/components/syncthru/translations/he.json
new file mode 100644
index 00000000000..3e7be115406
--- /dev/null
+++ b/homeassistant/components/syncthru/translations/he.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "data": {
+ "name": "\u05e9\u05dd",
+ "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05de\u05de\u05e9\u05e7 \u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8"
+ }
+ },
+ "user": {
+ "data": {
+ "name": "\u05e9\u05dd",
+ "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05de\u05de\u05e9\u05e7 \u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/syncthru/translations/hu.json b/homeassistant/components/syncthru/translations/hu.json
index 227b759ffaf..56e7c54203d 100644
--- a/homeassistant/components/syncthru/translations/hu.json
+++ b/homeassistant/components/syncthru/translations/hu.json
@@ -6,6 +6,7 @@
"error": {
"invalid_url": "\u00c9rv\u00e9nytelen URL"
},
+ "flow_title": "{name}",
"step": {
"confirm": {
"data": {
diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py
index c2de05b833e..1a3681daf32 100644
--- a/homeassistant/components/synology_dsm/config_flow.py
+++ b/homeassistant/components/synology_dsm/config_flow.py
@@ -17,12 +17,7 @@ import voluptuous as vol
from homeassistant import exceptions
from homeassistant.components import ssdp
-from homeassistant.config_entries import (
- CONN_CLASS_CLOUD_POLL,
- ConfigEntry,
- ConfigFlow,
- OptionsFlow,
-)
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import (
CONF_DISKS,
CONF_HOST,
@@ -92,7 +87,6 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
VERSION = 1
- CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL
@staticmethod
@callback
diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json
index f0d274c3bfe..932cc42db1d 100644
--- a/homeassistant/components/synology_dsm/translations/de.json
+++ b/homeassistant/components/synology_dsm/translations/de.json
@@ -10,7 +10,7 @@
"otp_failed": "Die zweistufige Authentifizierung ist fehlgeschlagen. Versuchen Sie es erneut mit einem neuen Code",
"unknown": "Unerwarteter Fehler"
},
- "flow_title": "Synology DSM {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"2sa": {
"data": {
diff --git a/homeassistant/components/synology_dsm/translations/he.json b/homeassistant/components/synology_dsm/translations/he.json
index 8135fba13e0..a671684a770 100644
--- a/homeassistant/components/synology_dsm/translations/he.json
+++ b/homeassistant/components/synology_dsm/translations/he.json
@@ -1,16 +1,34 @@
{
"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",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "missing_data": "\u05e0\u05ea\u05d5\u05e0\u05d9\u05dd \u05d7\u05e1\u05e8\u05d9\u05dd: \u05e0\u05d0 \u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05e0\u05d9\u05ea \u05de\u05d0\u05d5\u05d7\u05e8 \u05d9\u05d5\u05ea\u05e8 \u05d0\u05d5 \u05d1\u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05e8\u05ea",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "flow_title": "{name} ({host})",
"step": {
"link": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
- "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
- }
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
+ },
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?"
},
"user": {
"data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
- "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json
index c26fd349f06..e5af260449a 100644
--- a/homeassistant/components/synology_dsm/translations/hu.json
+++ b/homeassistant/components/synology_dsm/translations/hu.json
@@ -8,7 +8,7 @@
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
- "flow_title": "Synology DSM {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"2sa": {
"data": {
diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py
index cb78603f6dc..10ee4165295 100644
--- a/homeassistant/components/system_bridge/__init__.py
+++ b/homeassistant/components/system_bridge/__init__.py
@@ -59,7 +59,7 @@ SERVICE_OPEN_SCHEMA = vol.Schema(
)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up System Bridge from a config entry."""
client = Bridge(
diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py
index a74c060fccf..a93420bf6ae 100644
--- a/homeassistant/components/system_bridge/config_flow.py
+++ b/homeassistant/components/system_bridge/config_flow.py
@@ -65,7 +65,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for System Bridge."""
VERSION = 1
- CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self):
"""Initialize flow."""
diff --git a/homeassistant/components/system_bridge/translations/de.json b/homeassistant/components/system_bridge/translations/de.json
index 9f03951c429..dc85589dd32 100644
--- a/homeassistant/components/system_bridge/translations/de.json
+++ b/homeassistant/components/system_bridge/translations/de.json
@@ -16,7 +16,7 @@
"data": {
"api_key": "API-Schl\u00fcssel"
},
- "description": "Bitte gib den API-Schl\u00fcssel ein, den du in deiner Konfiguration f\u00fcr {Name} festgelegt hast."
+ "description": "Bitte gib den API-Schl\u00fcssel ein, den du in deiner Konfiguration f\u00fcr {name} festgelegt hast."
},
"user": {
"data": {
@@ -27,5 +27,6 @@
"description": "Bitte gib Verbindungsdaten ein."
}
}
- }
+ },
+ "title": "System-Br\u00fccke"
}
\ No newline at end of file
diff --git a/homeassistant/components/system_bridge/translations/he.json b/homeassistant/components/system_bridge/translations/he.json
new file mode 100644
index 00000000000..5eb235ffb8f
--- /dev/null
+++ b/homeassistant/components/system_bridge/translations/he.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "authenticate": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API"
+ },
+ "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05de\u05e4\u05ea\u05d7 API \u05e9\u05d4\u05d2\u05d3\u05e8\u05ea \u05d1\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc\u05da \u05e2\u05d1\u05d5\u05e8 {name} ."
+ },
+ "user": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ },
+ "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc\u05da."
+ }
+ }
+ },
+ "title": "\u05d2\u05e9\u05e8 \u05d4\u05de\u05e2\u05e8\u05db\u05ea"
+}
\ No newline at end of file
diff --git a/homeassistant/components/system_bridge/translations/hu.json b/homeassistant/components/system_bridge/translations/hu.json
new file mode 100644
index 00000000000..e8940bef26a
--- /dev/null
+++ b/homeassistant/components/system_bridge/translations/hu.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "flow_title": "{name}"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py
index 6e23faf606c..a218e627eb6 100644
--- a/homeassistant/components/systemmonitor/sensor.py
+++ b/homeassistant/components/systemmonitor/sensor.py
@@ -22,6 +22,7 @@ from homeassistant.const import (
DATA_GIBIBYTES,
DATA_MEBIBYTES,
DATA_RATE_MEGABYTES_PER_SECOND,
+ DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
EVENT_HOMEASSISTANT_STOP,
PERCENTAGE,
@@ -72,7 +73,7 @@ SENSOR_TYPES: dict[str, tuple[str, str | None, str | None, str | None, bool]] =
),
"ipv4_address": ("IPv4 address", "", "mdi:server-network", None, True),
"ipv6_address": ("IPv6 address", "", "mdi:server-network", None, True),
- "last_boot": ("Last boot", None, "mdi:clock", DEVICE_CLASS_TIMESTAMP, False),
+ "last_boot": ("Last boot", None, None, DEVICE_CLASS_TIMESTAMP, False),
"load_15m": ("Load (15m)", " ", CPU_ICON, None, False),
"load_1m": ("Load (1m)", " ", CPU_ICON, None, False),
"load_5m": ("Load (5m)", " ", CPU_ICON, None, False),
@@ -108,8 +109,8 @@ SENSOR_TYPES: dict[str, tuple[str, str | None, str | None, str | None, bool]] =
"processor_temperature": (
"Processor temperature",
TEMP_CELSIUS,
- CPU_ICON,
None,
+ DEVICE_CLASS_TEMPERATURE,
False,
),
"swap_free": ("Swap free", DATA_MEBIBYTES, "mdi:harddisk", None, False),
diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py
index 37ee3b47b9b..db42df62154 100644
--- a/homeassistant/components/tado/__init__.py
+++ b/homeassistant/components/tado/__init__.py
@@ -38,7 +38,7 @@ SCAN_INTERVAL = timedelta(minutes=5)
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Tado from a config entry."""
_async_import_options_from_data_if_missing(hass, entry)
diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py
index 87d2170eb75..e1219b5620b 100644
--- a/homeassistant/components/tado/sensor.py
+++ b/homeassistant/components/tado/sensor.py
@@ -139,7 +139,7 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
- if self.home_variable == "temperature":
+ if self.home_variable in ["temperature", "outdoor temperature"]:
return TEMP_CELSIUS
if self.home_variable == "solar percentage":
return PERCENTAGE
diff --git a/homeassistant/components/tado/translations/he.json b/homeassistant/components/tado/translations/he.json
index ac90b3264ea..c479d8488f2 100644
--- a/homeassistant/components/tado/translations/he.json
+++ b/homeassistant/components/tado/translations/he.json
@@ -1,5 +1,13 @@
{
"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",
+ "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": {
diff --git a/homeassistant/components/tag/translations/he.json b/homeassistant/components/tag/translations/he.json
new file mode 100644
index 00000000000..209a1f54193
--- /dev/null
+++ b/homeassistant/components/tag/translations/he.json
@@ -0,0 +1,3 @@
+{
+ "title": "\u05ea\u05d2"
+}
\ No newline at end of file
diff --git a/homeassistant/components/tag/trigger.py b/homeassistant/components/tag/trigger.py
index 4f6dd89a252..1984505f3a6 100644
--- a/homeassistant/components/tag/trigger.py
+++ b/homeassistant/components/tag/trigger.py
@@ -7,7 +7,7 @@ from homeassistant.helpers import config_validation as cv
from .const import DEVICE_ID, DOMAIN, EVENT_TAG_SCANNED, TAG_ID
-TRIGGER_SCHEMA = vol.Schema(
+TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): DOMAIN,
vol.Required(TAG_ID): vol.All(cv.ensure_list, [cv.string]),
@@ -18,7 +18,7 @@ TRIGGER_SCHEMA = vol.Schema(
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for tag_scanned events based on configuration."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
tag_ids = set(config[TAG_ID])
device_ids = set(config[DEVICE_ID]) if DEVICE_ID in config else None
@@ -35,10 +35,10 @@ async def async_attach_trigger(hass, config, action, automation_info):
job,
{
"trigger": {
+ **trigger_data,
"platform": DOMAIN,
"event": event,
"description": "Tag scanned",
- "id": trigger_id,
}
},
event.context,
diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py
index d4aca9b07ca..6a6f0324e1d 100644
--- a/homeassistant/components/tasmota/device_trigger.py
+++ b/homeassistant/components/tasmota/device_trigger.py
@@ -9,7 +9,7 @@ from hatasmota.trigger import TasmotaTrigger
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import event as event_trigger
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
@@ -28,7 +28,7 @@ CONF_DISCOVERY_ID = "discovery_id"
CONF_SUBTYPE = "subtype"
DEVICE = "device"
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): DEVICE,
vol.Required(CONF_DOMAIN): DOMAIN,
diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py
index 58a1ff1fb23..9af95049f79 100644
--- a/homeassistant/components/tasmota/light.py
+++ b/homeassistant/components/tasmota/light.py
@@ -12,14 +12,14 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_EFFECT,
- ATTR_RGB_COLOR,
- ATTR_RGBW_COLOR,
+ ATTR_HS_COLOR,
ATTR_TRANSITION,
+ ATTR_WHITE,
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
+ COLOR_MODE_HS,
COLOR_MODE_ONOFF,
- COLOR_MODE_RGB,
- COLOR_MODE_RGBW,
+ COLOR_MODE_WHITE,
SUPPORT_EFFECT,
SUPPORT_TRANSITION,
LightEntity,
@@ -60,6 +60,17 @@ def clamp(value):
return min(max(value, 0), 255)
+def scale_brightness(brightness):
+ """Scale brightness from 0..255 to 1..100."""
+ brightness_normalized = brightness / DEFAULT_BRIGHTNESS_MAX
+ device_brightness = min(
+ round(brightness_normalized * TASMOTA_BRIGHTNESS_MAX),
+ TASMOTA_BRIGHTNESS_MAX,
+ )
+ # Make sure the brightness is not rounded down to 0
+ return max(device_brightness, 1)
+
+
class TasmotaLight(
TasmotaAvailability,
TasmotaDiscoveryUpdate,
@@ -79,8 +90,7 @@ class TasmotaLight(
self._effect = None
self._white_value = None
self._flash_times = None
- self._rgb = None
- self._rgbw = None
+ self._hs = None
super().__init__(
**kwds,
@@ -101,14 +111,13 @@ class TasmotaLight(
light_type = self._tasmota_entity.light_type
if light_type in [LIGHT_TYPE_RGB, LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]:
- # Mark RGB support for RGBW light because we don't have control over the
+ # Mark HS support for RGBW light because we don't have direct control over the
# white channel, so the base component's RGB->RGBW translation does not work
- self._supported_color_modes.add(COLOR_MODE_RGB)
- self._color_mode = COLOR_MODE_RGB
+ self._supported_color_modes.add(COLOR_MODE_HS)
+ self._color_mode = COLOR_MODE_HS
if light_type == LIGHT_TYPE_RGBW:
- self._supported_color_modes.add(COLOR_MODE_RGBW)
- self._color_mode = COLOR_MODE_RGBW
+ self._supported_color_modes.add(COLOR_MODE_WHITE)
if light_type in [LIGHT_TYPE_COLDWARM, LIGHT_TYPE_RGBCW]:
self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
@@ -140,8 +149,8 @@ class TasmotaLight(
brightness = float(attributes["brightness"])
percent_bright = brightness / TASMOTA_BRIGHTNESS_MAX
self._brightness = percent_bright * 255
- if "color" in attributes:
- self._rgb = attributes["color"][0:3]
+ if "color_hs" in attributes:
+ self._hs = attributes["color_hs"]
if "color_temp" in attributes:
self._color_temp = attributes["color_temp"]
if "effect" in attributes:
@@ -150,10 +159,16 @@ class TasmotaLight(
white_value = float(attributes["white_value"])
percent_white = white_value / TASMOTA_BRIGHTNESS_MAX
self._white_value = percent_white * 255
- if self._tasmota_entity.light_type == LIGHT_TYPE_RGBCW:
- # Tasmota does not support RGBWW mode, set mode to ct or rgb
+ if self._tasmota_entity.light_type == LIGHT_TYPE_RGBW:
+ # Tasmota does not support RGBW mode, set mode to white or hs
if self._white_value == 0:
- self._color_mode = COLOR_MODE_RGB
+ self._color_mode = COLOR_MODE_HS
+ else:
+ self._color_mode = COLOR_MODE_WHITE
+ elif self._tasmota_entity.light_type == LIGHT_TYPE_RGBCW:
+ # Tasmota does not support RGBWW mode, set mode to ct or hs
+ if self._white_value == 0:
+ self._color_mode = COLOR_MODE_HS
else:
self._color_mode = COLOR_MODE_COLOR_TEMP
@@ -195,46 +210,16 @@ class TasmotaLight(
return self._tasmota_entity.effect_list
@property
- def rgb_color(self):
- """Return the rgb color value."""
- if self._rgb is None:
+ def hs_color(self):
+ """Return the hs color value."""
+ if self._hs is None:
return None
- rgb = self._rgb
- # Tasmota's RGB color is adjusted for brightness, compensate
- if self._brightness > 0:
- red_compensated = clamp(round(rgb[0] / self._brightness * 255))
- green_compensated = clamp(round(rgb[1] / self._brightness * 255))
- blue_compensated = clamp(round(rgb[2] / self._brightness * 255))
- else:
- red_compensated = 0
- green_compensated = 0
- blue_compensated = 0
- return [red_compensated, green_compensated, blue_compensated]
-
- @property
- def rgbw_color(self):
- """Return the rgbw color value."""
- if self._rgb is None or self._white_value is None:
- return None
- rgb = self._rgb
- # Tasmota's color is adjusted for brightness, compensate
- if self._brightness > 0:
- red_compensated = clamp(round(rgb[0] / self._brightness * 255))
- green_compensated = clamp(round(rgb[1] / self._brightness * 255))
- blue_compensated = clamp(round(rgb[2] / self._brightness * 255))
- white_compensated = clamp(round(self._white_value / self._brightness * 255))
- else:
- red_compensated = 0
- green_compensated = 0
- blue_compensated = 0
- white_compensated = 0
- return [red_compensated, green_compensated, blue_compensated, white_compensated]
+ hs_color = self._hs
+ return [hs_color[0], hs_color[1]]
@property
def force_update(self):
"""Force update."""
- if self.color_mode == COLOR_MODE_RGBW:
- return True
return False
@property
@@ -258,29 +243,18 @@ class TasmotaLight(
attributes = {}
- if ATTR_RGB_COLOR in kwargs and COLOR_MODE_RGB in supported_color_modes:
- rgb = kwargs[ATTR_RGB_COLOR]
- attributes["color"] = [rgb[0], rgb[1], rgb[2]]
+ if ATTR_HS_COLOR in kwargs and COLOR_MODE_HS in supported_color_modes:
+ hs_color = kwargs[ATTR_HS_COLOR]
+ attributes["color_hs"] = [hs_color[0], hs_color[1]]
- if ATTR_RGBW_COLOR in kwargs and COLOR_MODE_RGBW in supported_color_modes:
- rgbw = kwargs[ATTR_RGBW_COLOR]
- attributes["color"] = [rgbw[0], rgbw[1], rgbw[2], rgbw[3]]
- # Tasmota does not support direct RGBW control, the light must be set to
- # either white mode or color mode. Set the mode to white if white channel
- # is on, and to color otherwise
+ if ATTR_WHITE in kwargs and COLOR_MODE_WHITE in supported_color_modes:
+ attributes["white_value"] = scale_brightness(kwargs[ATTR_WHITE])
if ATTR_TRANSITION in kwargs:
attributes["transition"] = kwargs[ATTR_TRANSITION]
if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes):
- brightness_normalized = kwargs[ATTR_BRIGHTNESS] / DEFAULT_BRIGHTNESS_MAX
- device_brightness = min(
- round(brightness_normalized * TASMOTA_BRIGHTNESS_MAX),
- TASMOTA_BRIGHTNESS_MAX,
- )
- # Make sure the brightness is not rounded down to 0
- device_brightness = max(device_brightness, 1)
- attributes["brightness"] = device_brightness
+ attributes["brightness"] = scale_brightness(kwargs[ATTR_BRIGHTNESS])
if ATTR_COLOR_TEMP in kwargs and COLOR_MODE_COLOR_TEMP in supported_color_modes:
attributes["color_temp"] = int(kwargs[ATTR_COLOR_TEMP])
diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json
index cb869b6099c..f87e0c189f1 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.2.14"],
+ "requirements": ["hatasmota==0.2.19"],
"dependencies": ["mqtt"],
"mqtt": ["tasmota/discovery/#"],
"codeowners": ["@emontnemery"],
diff --git a/homeassistant/components/tasmota/translations/de.json b/homeassistant/components/tasmota/translations/de.json
index 86c3383f912..7e654d982c5 100644
--- a/homeassistant/components/tasmota/translations/de.json
+++ b/homeassistant/components/tasmota/translations/de.json
@@ -3,6 +3,9 @@
"abort": {
"single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
+ "error": {
+ "invalid_discovery_topic": "Ung\u00fcltiges Discovery-Topic-Pr\u00e4fix."
+ },
"step": {
"config": {
"data": {
diff --git a/homeassistant/components/tasmota/translations/he.json b/homeassistant/components/tasmota/translations/he.json
new file mode 100644
index 00000000000..7853c226b33
--- /dev/null
+++ b/homeassistant/components/tasmota/translations/he.json
@@ -0,0 +1,16 @@
+{
+ "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": {
+ "config": {
+ "description": "\u05d0\u05e0\u05d0 \u05d4\u05db\u05e0\u05e1 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea Tasmota.",
+ "title": "Tasmota"
+ },
+ "confirm": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Tasmota?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py
index 62cdd5066ad..5c439651ed5 100644
--- a/homeassistant/components/ted5000/sensor.py
+++ b/homeassistant/components/ted5000/sensor.py
@@ -7,7 +7,11 @@ import requests
import voluptuous as vol
import xmltodict
-from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.components.sensor import (
+ PLATFORM_SCHEMA,
+ STATE_CLASS_MEASUREMENT,
+ SensorEntity,
+)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, POWER_WATT, VOLT
from homeassistant.helpers import config_validation as cv
from homeassistant.util import Throttle
@@ -52,6 +56,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class Ted5000Sensor(SensorEntity):
"""Implementation of a Ted5000 sensor."""
+ _attr_state_class = STATE_CLASS_MEASUREMENT
+
def __init__(self, gateway, name, mtu, unit):
"""Initialize the sensor."""
units = {POWER_WATT: "power", VOLT: "voltage"}
diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py
index fe3728ba91b..4231bcc46af 100644
--- a/homeassistant/components/telegram_bot/__init__.py
+++ b/homeassistant/components/telegram_bot/__init__.py
@@ -26,6 +26,7 @@ from homeassistant.const import (
CONF_API_KEY,
CONF_PLATFORM,
CONF_URL,
+ HTTP_BEARER_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION,
)
from homeassistant.exceptions import TemplateError
@@ -68,6 +69,7 @@ ATTR_USERNAME = "username"
ATTR_VERIFY_SSL = "verify_ssl"
ATTR_TIMEOUT = "timeout"
ATTR_MESSAGE_TAG = "message_tag"
+ATTR_CHANNEL_POST = "channel_post"
CONF_ALLOWED_CHAT_IDS = "allowed_chat_ids"
CONF_PROXY_URL = "proxy_url"
@@ -254,7 +256,9 @@ def load_data(
if url is not None:
# Load data from URL
params = {"timeout": 15}
- if username is not None and password is not None:
+ if authentication == HTTP_BEARER_AUTHENTICATION and password is not None:
+ params["headers"] = {"Authorization": f"Bearer {password}"}
+ elif username is not None and password is not None:
if authentication == HTTP_DIGEST_AUTHENTICATION:
params["auth"] = HTTPDigestAuth(username, password)
else:
@@ -866,6 +870,31 @@ class BaseTelegramBotEntity:
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:
@@ -916,6 +945,15 @@ class BaseTelegramBotEntity:
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)
+ return True
_LOGGER.warning("Message with unknown data received: %s", data)
return True
diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml
index dc3e9dde2d3..ea406cfdf96 100644
--- a/homeassistant/components/telegram_bot/services.yaml
+++ b/homeassistant/components/telegram_bot/services.yaml
@@ -29,9 +29,9 @@ send_message:
selector:
select:
options:
- - 'html'
- - 'markdown'
- - 'markdown2'
+ - "html"
+ - "markdown"
+ - "markdown2"
disable_notification:
name: Disable notification
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
@@ -65,7 +65,7 @@ send_message:
object:
message_tag:
name: Message tag
- description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}'
+ description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
example: "msg_to_edit"
selector:
text:
@@ -94,16 +94,25 @@ send_photo:
text:
username:
name: Username
- description: Username for a URL which require HTTP basic authentication.
+ description: Username for a URL which require HTTP authentication.
example: myuser
selector:
text:
password:
name: Password
- description: Password for a URL which require HTTP basic authentication.
+ description: Password (or bearer token) for a URL which require HTTP authentication.
example: myuser_pwd
selector:
text:
+ authentication:
+ name: Authentication method
+ description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`.
+ default: digest
+ selector:
+ select:
+ options:
+ - "digest"
+ - "bearer_token"
target:
name: Target
description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
@@ -116,9 +125,9 @@ send_photo:
selector:
select:
options:
- - 'html'
- - 'markdown'
- - 'markdown2'
+ - "html"
+ - "markdown"
+ - "markdown2"
disable_notification:
name: Disable notification
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
@@ -151,7 +160,7 @@ send_photo:
object:
message_tag:
name: Message tag
- description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}'
+ description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
example: "msg_to_edit"
selector:
text:
@@ -174,16 +183,25 @@ send_sticker:
text:
username:
name: Username
- description: Username for a URL which require HTTP basic authentication.
+ description: Username for a URL which require HTTP authentication.
example: myuser
selector:
text:
password:
name: Password
- description: Password for a URL which require HTTP basic authentication.
+ description: Password (or bearer token) for a URL which require HTTP authentication.
example: myuser_pwd
selector:
text:
+ authentication:
+ name: Authentication method
+ description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`.
+ default: digest
+ selector:
+ select:
+ options:
+ - "digest"
+ - "bearer_token"
target:
name: Target
description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
@@ -222,7 +240,7 @@ send_sticker:
object:
message_tag:
name: Message tag
- description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}'
+ description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
example: "msg_to_edit"
selector:
text:
@@ -251,16 +269,25 @@ send_animation:
text:
username:
name: Username
- description: Username for a URL which require HTTP basic authentication.
+ description: Username for a URL which require HTTP authentication.
example: myuser
selector:
text:
password:
name: Password
- description: Password for a URL which require HTTP basic authentication.
+ description: Password (or bearer token) for a URL which require HTTP authentication.
example: myuser_pwd
selector:
text:
+ authentication:
+ name: Authentication method
+ description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`.
+ default: digest
+ selector:
+ select:
+ options:
+ - "digest"
+ - "bearer_token"
target:
name: Target
description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
@@ -273,9 +300,9 @@ send_animation:
selector:
select:
options:
- - 'html'
- - 'markdown'
- - 'markdown2'
+ - "html"
+ - "markdown"
+ - "markdown2"
disable_notification:
name: Disable notification
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
@@ -331,16 +358,25 @@ send_video:
text:
username:
name: Username
- description: Username for a URL which require HTTP basic authentication.
+ description: Username for a URL which require HTTP authentication.
example: myuser
selector:
text:
password:
name: Password
- description: Password for a URL which require HTTP basic authentication.
+ description: Password (or bearer token) for a URL which require HTTP authentication.
example: myuser_pwd
selector:
text:
+ authentication:
+ name: Authentication method
+ description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`.
+ default: digest
+ selector:
+ select:
+ options:
+ - "digest"
+ - "bearer_token"
target:
name: Target
description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
@@ -353,9 +389,9 @@ send_video:
selector:
select:
options:
- - 'html'
- - 'markdown'
- - 'markdown2'
+ - "html"
+ - "markdown"
+ - "markdown2"
disable_notification:
name: Disable notification
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
@@ -388,7 +424,7 @@ send_video:
object:
message_tag:
name: Message tag
- description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}'
+ description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
example: "msg_to_edit"
selector:
text:
@@ -417,16 +453,25 @@ send_voice:
text:
username:
name: Username
- description: Username for a URL which require HTTP basic authentication.
+ description: Username for a URL which require HTTP authentication.
example: myuser
selector:
text:
password:
name: Password
- description: Password for a URL which require HTTP basic authentication.
+ description: Password (or bearer token) for a URL which require HTTP authentication.
example: myuser_pwd
selector:
text:
+ authentication:
+ name: Authentication method
+ description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`.
+ default: digest
+ selector:
+ select:
+ options:
+ - "digest"
+ - "bearer_token"
target:
name: Target
description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
@@ -465,7 +510,7 @@ send_voice:
object:
message_tag:
name: Message tag
- description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}'
+ description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
example: "msg_to_edit"
selector:
text:
@@ -494,16 +539,25 @@ send_document:
text:
username:
name: Username
- description: Username for a URL which require HTTP basic authentication.
+ description: Username for a URL which require HTTP authentication.
example: myuser
selector:
text:
password:
name: Password
- description: Password for a URL which require HTTP basic authentication.
+ description: Password (or bearer token) for a URL which require HTTP authentication.
example: myuser_pwd
selector:
text:
+ authentication:
+ name: Authentication method
+ description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`.
+ default: digest
+ selector:
+ select:
+ options:
+ - "digest"
+ - "bearer_token"
target:
name: Target
description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
@@ -516,9 +570,9 @@ send_document:
selector:
select:
options:
- - 'html'
- - 'markdown'
- - 'markdown2'
+ - "html"
+ - "markdown"
+ - "markdown2"
disable_notification:
name: Disable notification
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
@@ -551,7 +605,7 @@ send_document:
object:
message_tag:
name: Message tag
- description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}'
+ description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
example: "msg_to_edit"
selector:
text:
@@ -569,7 +623,7 @@ send_location:
min: -90
max: 90
step: 0.001
- unit_of_measurement: '°'
+ unit_of_measurement: "°"
longitude:
name: Longitude
description: The longitude to send.
@@ -579,7 +633,7 @@ send_location:
min: -180
max: 180
step: 0.001
- unit_of_measurement: '°'
+ unit_of_measurement: "°"
target:
name: Target
description: An array of pre-authorized chat_ids to send the location to. If not present, first allowed chat_id is the default.
@@ -613,7 +667,7 @@ send_location:
object:
message_tag:
name: Message tag
- description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}'
+ description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
example: "msg_to_edit"
selector:
text:
@@ -654,9 +708,9 @@ edit_message:
selector:
select:
options:
- - 'html'
- - 'markdown'
- - 'markdown2'
+ - "html"
+ - "markdown"
+ - "markdown2"
disable_web_page_preview:
name: Disable web page preview
description: Disables link previews for links in the message.
diff --git a/homeassistant/components/tellduslive/translations/he.json b/homeassistant/components/tellduslive/translations/he.json
new file mode 100644
index 00000000000..db5a0aad8d9
--- /dev/null
+++ b/homeassistant/components/tellduslive/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ },
+ "description": "\u05e8\u05d9\u05e7",
+ "title": "\u05d1\u05d7\u05e8 \u05e0\u05e7\u05d5\u05d3\u05ea \u05e7\u05e6\u05d4."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py
index 661953bcfa5..31896e930e4 100644
--- a/homeassistant/components/template/const.py
+++ b/homeassistant/components/template/const.py
@@ -25,3 +25,4 @@ CONF_AVAILABILITY = "availability"
CONF_ATTRIBUTES = "attributes"
CONF_PICTURE = "picture"
CONF_OBJECT_ID = "object_id"
+CONF_STATE_CLASS = "state_class"
diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py
index f546c5dc4da..b8ebe03ceba 100644
--- a/homeassistant/components/template/light.py
+++ b/homeassistant/components/template/light.py
@@ -595,7 +595,7 @@ class LightTemplate(TemplateEntity, LightEntity):
# This behavior is legacy
self._state = False
if not self._availability_template:
- self._available = True
+ self._attr_available = True
return
if isinstance(result, bool):
diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py
index 56e0e11edb0..a887890510a 100644
--- a/homeassistant/components/template/sensor.py
+++ b/homeassistant/components/template/sensor.py
@@ -8,6 +8,7 @@ from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
ENTITY_ID_FORMAT,
PLATFORM_SCHEMA,
+ STATE_CLASSES_SCHEMA,
SensorEntity,
)
from homeassistant.const import (
@@ -37,6 +38,7 @@ from .const import (
CONF_AVAILABILITY_TEMPLATE,
CONF_OBJECT_ID,
CONF_PICTURE,
+ CONF_STATE_CLASS,
CONF_TRIGGER,
)
from .template_entity import TemplateEntity
@@ -64,6 +66,7 @@ SENSOR_SCHEMA = vol.Schema(
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
+ vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
}
)
@@ -159,6 +162,7 @@ def _async_create_template_tracking_entities(
device_class = entity_conf.get(CONF_DEVICE_CLASS)
attribute_templates = entity_conf.get(CONF_ATTRIBUTES, {})
unique_id = entity_conf.get(CONF_UNIQUE_ID)
+ state_class = entity_conf.get(CONF_STATE_CLASS)
if unique_id and unique_id_prefix:
unique_id = f"{unique_id_prefix}-{unique_id}"
@@ -176,6 +180,7 @@ def _async_create_template_tracking_entities(
device_class,
attribute_templates,
unique_id,
+ state_class,
)
)
@@ -224,6 +229,7 @@ class SensorTemplate(TemplateEntity, SensorEntity):
device_class: str | None,
attribute_templates: dict[str, template.Template],
unique_id: str | None,
+ state_class: str | None,
) -> None:
"""Initialize the sensor."""
super().__init__(
@@ -237,61 +243,38 @@ class SensorTemplate(TemplateEntity, SensorEntity):
ENTITY_ID_FORMAT, object_id, hass=hass
)
- self._name: str | None = None
self._friendly_name_template = friendly_name_template
# Try to render the name as it can influence the entity ID
if friendly_name_template:
friendly_name_template.hass = hass
try:
- self._name = friendly_name_template.async_render(parse_result=False)
+ self._attr_name = friendly_name_template.async_render(
+ parse_result=False
+ )
except template.TemplateError:
pass
- self._unit_of_measurement = unit_of_measurement
+ self._attr_unit_of_measurement = unit_of_measurement
self._template = state_template
- self._state = None
- self._device_class = device_class
-
- self._unique_id = unique_id
+ self._attr_device_class = device_class
+ self._attr_state_class = state_class
+ self._attr_unique_id = unique_id
async def async_added_to_hass(self):
"""Register callbacks."""
- self.add_template_attribute("_state", self._template, None, self._update_state)
+ self.add_template_attribute(
+ "_attr_state", self._template, None, self._update_state
+ )
if self._friendly_name_template and not self._friendly_name_template.is_static:
- self.add_template_attribute("_name", self._friendly_name_template)
+ self.add_template_attribute("_attr_name", self._friendly_name_template)
await super().async_added_to_hass()
@callback
def _update_state(self, result):
super()._update_state(result)
- self._state = None if isinstance(result, TemplateError) else result
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def unique_id(self):
- """Return the unique id of this sensor."""
- return self._unique_id
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def device_class(self) -> str | None:
- """Return the device class of the sensor."""
- return self._device_class
-
- @property
- def unit_of_measurement(self):
- """Return the unit_of_measurement of the device."""
- return self._unit_of_measurement
+ self._attr_state = None if isinstance(result, TemplateError) else result
class TriggerSensorEntity(TriggerEntity, SensorEntity):
@@ -304,3 +287,8 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity):
def state(self) -> str | None:
"""Return state of the sensor."""
return self._rendered.get(CONF_STATE)
+
+ @property
+ def state_class(self) -> str | None:
+ """Sensor state class."""
+ return self._config.get(CONF_STATE_CLASS)
diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py
index 522eb7d89ba..7bf6d6109be 100644
--- a/homeassistant/components/template/template_entity.py
+++ b/homeassistant/components/template/template_entity.py
@@ -112,6 +112,8 @@ class _TemplateAttribute:
class TemplateEntity(Entity):
"""Entity that uses templates to calculate attributes."""
+ _attr_should_poll = False
+
def __init__(
self,
*,
@@ -124,54 +126,27 @@ class TemplateEntity(Entity):
self._template_attrs = {}
self._async_update = None
self._attribute_templates = attribute_templates
- self._attributes = {}
+ self._attr_extra_state_attributes = {}
self._availability_template = availability_template
- self._available = True
+ self._attr_available = True
self._icon_template = icon_template
self._entity_picture_template = entity_picture_template
- self._icon = None
- self._entity_picture = None
self._self_ref_update_count = 0
- @property
- def should_poll(self):
- """No polling needed."""
- return False
-
@callback
def _update_available(self, result):
if isinstance(result, TemplateError):
- self._available = True
+ self._attr_available = True
return
- self._available = result_as_boolean(result)
+ self._attr_available = result_as_boolean(result)
@callback
def _update_state(self, result):
if self._availability_template:
return
- self._available = not isinstance(result, TemplateError)
-
- @property
- def available(self) -> bool:
- """Return if the device is available."""
- return self._available
-
- @property
- def icon(self):
- """Return the icon to use in the frontend, if any."""
- return self._icon
-
- @property
- def entity_picture(self):
- """Return the entity_picture to use in the frontend, if any."""
- return self._entity_picture
-
- @property
- def extra_state_attributes(self):
- """Return the state attributes."""
- return self._attributes
+ self._attr_available = not isinstance(result, TemplateError)
@callback
def _add_attribute_template(self, attribute_key, attribute_template):
@@ -179,7 +154,7 @@ class TemplateEntity(Entity):
def _update_attribute(result):
attr_result = None if isinstance(result, TemplateError) else result
- self._attributes[attribute_key] = attr_result
+ self._attr_extra_state_attributes[attribute_key] = attr_result
self.add_template_attribute(
attribute_key, attribute_template, None, _update_attribute
@@ -271,18 +246,21 @@ class TemplateEntity(Entity):
"""Run when entity about to be added to hass."""
if self._availability_template is not None:
self.add_template_attribute(
- "_available", self._availability_template, None, self._update_available
+ "_attr_available",
+ self._availability_template,
+ None,
+ self._update_available,
)
if self._attribute_templates is not None:
for key, value in self._attribute_templates.items():
self._add_attribute_template(key, value)
if self._icon_template is not None:
self.add_template_attribute(
- "_icon", self._icon_template, vol.Or(cv.whitespace, cv.icon)
+ "_attr_icon", self._icon_template, vol.Or(cv.whitespace, cv.icon)
)
if self._entity_picture_template is not None:
self.add_template_attribute(
- "_entity_picture", self._entity_picture_template
+ "_attr_entity_picture", self._entity_picture_template
)
if self.hass.state == CoreState.running:
await self._async_template_startup()
diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py
index 998984e0b9a..6db25da76ab 100644
--- a/homeassistant/components/template/trigger.py
+++ b/homeassistant/components/template/trigger.py
@@ -18,7 +18,7 @@ from homeassistant.helpers.template import result_as_boolean
_LOGGER = logging.getLogger(__name__)
-TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema(
+TRIGGER_SCHEMA = IF_ACTION_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "template",
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
@@ -31,7 +31,7 @@ async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type="template"
):
"""Listen for state changes based on configuration."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
value_template = config.get(CONF_VALUE_TEMPLATE)
value_template.hass = hass
time_delta = config.get(CONF_FOR)
@@ -99,9 +99,9 @@ async def async_attach_trigger(
"to_state": to_s,
}
trigger_variables = {
+ **trigger_data,
"for": time_delta,
"description": description,
- "id": trigger_id,
}
@callback
diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py
index ed7919d174e..78c51d2009c 100644
--- a/homeassistant/components/template/vacuum.py
+++ b/homeassistant/components/template/vacuum.py
@@ -362,7 +362,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity):
# This is legacy behavior
self._state = STATE_UNKNOWN
if not self._availability_template:
- self._available = True
+ self._attr_available = True
return
# Validate state
diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json
index 74c10af363d..1b161b4aec8 100644
--- a/homeassistant/components/tensorflow/manifest.json
+++ b/homeassistant/components/tensorflow/manifest.json
@@ -7,7 +7,7 @@
"tf-models-official==2.3.0",
"pycocotools==2.0.1",
"numpy==1.20.3",
- "pillow==8.1.2"
+ "pillow==8.2.0"
],
"codeowners": [],
"iot_class": "local_polling"
diff --git a/homeassistant/components/tesla/translations/he.json b/homeassistant/components/tesla/translations/he.json
index ac90b3264ea..9f3eeb2fc21 100644
--- a/homeassistant/components/tesla/translations/he.json
+++ b/homeassistant/components/tesla/translations/he.json
@@ -1,10 +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",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\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": {
"user": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
- "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ "username": "\u05d3\u05d5\u05d0\"\u05dc"
}
}
}
diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py
index 5bd6f77253b..1a53a599394 100644
--- a/homeassistant/components/threshold/binary_sensor.py
+++ b/homeassistant/components/threshold/binary_sensor.py
@@ -13,6 +13,7 @@ from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ENTITY_ID,
CONF_NAME,
+ STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import callback
@@ -100,7 +101,9 @@ class ThresholdSensor(BinarySensorEntity):
try:
self.sensor_value = (
- None if new_state.state == STATE_UNKNOWN else float(new_state.state)
+ None
+ if new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]
+ else float(new_state.state)
)
except (ValueError, TypeError):
self.sensor_value = None
diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json
index 57b329765a9..a915db8a665 100644
--- a/homeassistant/components/tibber/manifest.json
+++ b/homeassistant/components/tibber/manifest.json
@@ -2,7 +2,7 @@
"domain": "tibber",
"name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber",
- "requirements": ["pyTibber==0.17.0"],
+ "requirements": ["pyTibber==0.18.0"],
"codeowners": ["@danielhiversen"],
"quality_scale": "silver",
"config_flow": true,
diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py
index 660bbb741b0..bf6218dcb31 100644
--- a/homeassistant/components/tibber/sensor.py
+++ b/homeassistant/components/tibber/sensor.py
@@ -1,6 +1,6 @@
"""Support for Tibber sensors."""
import asyncio
-from datetime import datetime, timedelta
+from datetime import timedelta
import logging
from random import randrange
@@ -9,7 +9,9 @@ import aiohttp
from homeassistant.components.sensor import (
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_MONETARY,
DEVICE_CLASS_POWER,
+ DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
@@ -18,6 +20,7 @@ from homeassistant.components.sensor import (
from homeassistant.const import (
ELECTRICAL_CURRENT_AMPERE,
ENERGY_KILO_WATT_HOUR,
+ PERCENTAGE,
POWER_WATT,
SIGNAL_STRENGTH_DECIBELS,
VOLT,
@@ -126,7 +129,18 @@ RT_SENSOR_MAP = {
SIGNAL_STRENGTH_DECIBELS,
STATE_CLASS_MEASUREMENT,
],
- "accumulatedCost": ["accumulated cost", None, None, STATE_CLASS_MEASUREMENT],
+ "accumulatedCost": [
+ "accumulated cost",
+ DEVICE_CLASS_MONETARY,
+ None,
+ STATE_CLASS_MEASUREMENT,
+ ],
+ "powerFactor": [
+ "power factor",
+ DEVICE_CLASS_POWER_FACTOR,
+ PERCENTAGE,
+ STATE_CLASS_MEASUREMENT,
+ ],
}
@@ -311,7 +325,7 @@ class TibberSensorRT(TibberSensor):
"last meter consumption",
"last meter production",
]:
- self._attr_last_reset = datetime.fromtimestamp(0)
+ self._attr_last_reset = dt_util.utc_from_timestamp(0)
elif self._sensor_name in [
"accumulated consumption",
"accumulated production",
@@ -395,6 +409,8 @@ class TibberRtDataHandler:
for sensor_type, state in live_measurement.items():
if state is None or sensor_type not in RT_SENSOR_MAP:
continue
+ if sensor_type == "powerFactor":
+ state *= 100.0
if sensor_type in self._entities:
async_dispatcher_send(
self.hass,
diff --git a/homeassistant/components/tibber/translations/he.json b/homeassistant/components/tibber/translations/he.json
index a2804e54143..780c7990217 100644
--- a/homeassistant/components/tibber/translations/he.json
+++ b/homeassistant/components/tibber/translations/he.json
@@ -4,6 +4,8 @@
"already_configured": "\u05d7\u05e9\u05d1\u05d5\u05df Tibber \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
},
"error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
"timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05beTibber"
},
"step": {
diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py
index 91e1567cd65..7faefe4d275 100644
--- a/homeassistant/components/tile/__init__.py
+++ b/homeassistant/components/tile/__init__.py
@@ -6,8 +6,10 @@ from pytile import async_login
from pytile.errors import InvalidAuthError, SessionExpiredError, TileError
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.entity_registry import async_migrate_entries
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.async_ import gather_with_concurrency
@@ -33,6 +35,32 @@ async def async_setup_entry(hass, entry):
hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {}
hass.data[DOMAIN][DATA_TILE][entry.entry_id] = {}
+ @callback
+ def async_migrate_callback(entity_entry):
+ """
+ Define a callback to migrate appropriate Tile entities to new unique IDs.
+
+ Old: tile_{uuid}
+ New: {username}_{uuid}
+ """
+ if entity_entry.unique_id.startswith(entry.data[CONF_USERNAME]):
+ return
+
+ new_unique_id = f"{entry.data[CONF_USERNAME]}_".join(
+ entity_entry.unique_id.split(f"{DOMAIN}_")
+ )
+
+ LOGGER.debug(
+ "Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
+ entity_entry.entity_id,
+ entity_entry.unique_id,
+ new_unique_id,
+ )
+
+ return {"new_unique_id": new_unique_id}
+
+ await async_migrate_entries(hass, entry.entry_id, async_migrate_callback)
+
websession = aiohttp_client.async_get_clientsession(hass)
try:
diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py
index 7571e235ef1..add6e5f94a0 100644
--- a/homeassistant/components/tile/device_tracker.py
+++ b/homeassistant/components/tile/device_tracker.py
@@ -30,7 +30,9 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(
[
TileDeviceTracker(
- hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][tile_uuid], tile
+ entry,
+ hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][tile_uuid],
+ tile,
)
for tile_uuid, tile in hass.data[DOMAIN][DATA_TILE][entry.entry_id].items()
]
@@ -61,10 +63,11 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
class TileDeviceTracker(CoordinatorEntity, TrackerEntity):
"""Representation of a network infrastructure device."""
- def __init__(self, coordinator, tile):
+ def __init__(self, entry, coordinator, tile):
"""Initialize."""
super().__init__(coordinator)
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
+ self._entry = entry
self._tile = tile
@property
@@ -116,7 +119,7 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity):
@property
def unique_id(self):
"""Return the unique ID of the entity."""
- return f"tile_{self._tile.uuid}"
+ return f"{self._entry.data[CONF_USERNAME]}_{self._tile.uuid}"
@property
def source_type(self):
diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json
index a17c099509e..e8d386f4a88 100644
--- a/homeassistant/components/tile/manifest.json
+++ b/homeassistant/components/tile/manifest.json
@@ -3,7 +3,7 @@
"name": "Tile",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tile",
- "requirements": ["pytile==5.2.0"],
+ "requirements": ["pytile==5.2.2"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/tile/translations/he.json b/homeassistant/components/tile/translations/he.json
new file mode 100644
index 00000000000..adb5e510107
--- /dev/null
+++ b/homeassistant/components/tile/translations/he.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05d3\u05d5\u05d0\"\u05dc"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py
index 8b9379d2186..86aeff7c554 100644
--- a/homeassistant/components/todoist/calendar.py
+++ b/homeassistant/components/todoist/calendar.py
@@ -230,7 +230,7 @@ def _parse_due_date(data: dict, gmt_string) -> datetime:
"""Parse the due date dict into a datetime object."""
# Add time information to date only strings.
if len(data["date"]) == 10:
- data["date"] += "T00:00:00"
+ return datetime.fromisoformat(data["date"]).replace(tzinfo=dt.UTC)
if dt.parse_datetime(data["date"]).tzinfo is None:
data["date"] += gmt_string
return dt.as_utc(dt.parse_datetime(data["date"]))
diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py
index 4a55911dcfc..de756225d57 100644
--- a/homeassistant/components/toon/binary_sensor.py
+++ b/homeassistant/components/toon/binary_sensor.py
@@ -61,27 +61,21 @@ class ToonBinarySensor(ToonEntity, BinarySensorEntity):
def __init__(self, coordinator: ToonDataUpdateCoordinator, *, key: str) -> None:
"""Initialize the Toon sensor."""
+ super().__init__(coordinator)
self.key = key
- super().__init__(
- coordinator,
- enabled_default=BINARY_SENSOR_ENTITIES[key][ATTR_DEFAULT_ENABLED],
- icon=BINARY_SENSOR_ENTITIES[key][ATTR_ICON],
- name=BINARY_SENSOR_ENTITIES[key][ATTR_NAME],
+ sensor = BINARY_SENSOR_ENTITIES[key]
+ self._attr_name = sensor[ATTR_NAME]
+ self._attr_icon = sensor.get(ATTR_ICON)
+ self._attr_entity_registry_enabled_default = sensor.get(
+ ATTR_DEFAULT_ENABLED, True
+ )
+ self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS)
+ self._attr_unique_id = (
+ # This unique ID is a bit ugly and contains unneeded information.
+ # It is here for legacy / backward compatible reasons.
+ f"{DOMAIN}_{coordinator.data.agreement.agreement_id}_binary_sensor_{key}"
)
-
- @property
- def unique_id(self) -> str:
- """Return the unique ID for this binary sensor."""
- agreement_id = self.coordinator.data.agreement.agreement_id
- # This unique ID is a bit ugly and contains unneeded information.
- # It is here for legacy / backward compatible reasons.
- return f"{DOMAIN}_{agreement_id}_binary_sensor_{self.key}"
-
- @property
- def device_class(self) -> str:
- """Return the device class."""
- return BINARY_SENSOR_ENTITIES[self.key][ATTR_DEVICE_CLASS]
@property
def is_on(self) -> bool | None:
@@ -94,7 +88,7 @@ class ToonBinarySensor(ToonEntity, BinarySensorEntity):
if value is None:
return None
- if BINARY_SENSOR_ENTITIES[self.key][ATTR_INVERTED]:
+ if BINARY_SENSOR_ENTITIES[self.key].get(ATTR_INVERTED, False):
return not value
return value
diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py
index 1c7bde7d9e5..a1201f85f7c 100644
--- a/homeassistant/components/toon/climate.py
+++ b/homeassistant/components/toon/climate.py
@@ -26,6 +26,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.core import HomeAssistant
+from . import ToonDataUpdateCoordinator
from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN
from .helpers import toon_exception_handler
from .models import ToonDisplayDeviceEntity
@@ -36,36 +37,36 @@ async def async_setup_entry(
) -> None:
"""Set up a Toon binary sensors based on a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
- async_add_entities(
- [ToonThermostatDevice(coordinator, name="Thermostat", icon="mdi:thermostat")]
- )
+ async_add_entities([ToonThermostatDevice(coordinator)])
class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity):
"""Representation of a Toon climate device."""
- @property
- def unique_id(self) -> str:
- """Return the unique ID for this thermostat."""
- agreement_id = self.coordinator.data.agreement.agreement_id
- # This unique ID is a bit ugly and contains unneeded information.
- # It is here for lecagy / backward compatible reasons.
- return f"{DOMAIN}_{agreement_id}_climate"
+ _attr_hvac_mode = HVAC_MODE_HEAT
+ _attr_icon = "mdi:thermostat"
+ _attr_max_temp = DEFAULT_MAX_TEMP
+ _attr_min_temp = DEFAULT_MIN_TEMP
+ _attr_name = "Thermostat"
+ _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
+ _attr_temperature_unit = TEMP_CELSIUS
- @property
- def supported_features(self) -> int:
- """Return the list of supported features."""
- return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
-
- @property
- def hvac_mode(self) -> str:
- """Return hvac operation ie. heat, cool mode."""
- return HVAC_MODE_HEAT
-
- @property
- def hvac_modes(self) -> list[str]:
- """Return the list of available hvac operation modes."""
- return [HVAC_MODE_HEAT]
+ def __init__(
+ self,
+ coordinator: ToonDataUpdateCoordinator,
+ ) -> None:
+ """Initialize Toon climate entity."""
+ super().__init__(coordinator)
+ self._attr_hvac_modes = [HVAC_MODE_HEAT]
+ self._attr_preset_modes = [
+ PRESET_AWAY,
+ PRESET_COMFORT,
+ PRESET_HOME,
+ PRESET_SLEEP,
+ ]
+ self._attr_unique_id = (
+ f"{DOMAIN}_{coordinator.data.agreement.agreement_id}_climate"
+ )
@property
def hvac_action(self) -> str | None:
@@ -74,11 +75,6 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity):
return CURRENT_HVAC_HEAT
return CURRENT_HVAC_IDLE
- @property
- def temperature_unit(self) -> str:
- """Return the unit of measurement."""
- return TEMP_CELSIUS
-
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
@@ -90,11 +86,6 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity):
}
return mapping.get(self.coordinator.data.thermostat.active_state)
- @property
- def preset_modes(self) -> list[str]:
- """Return a list of available preset modes."""
- return [PRESET_AWAY, PRESET_COMFORT, PRESET_HOME, PRESET_SLEEP]
-
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
@@ -105,16 +96,6 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity):
"""Return the temperature we try to reach."""
return self.coordinator.data.thermostat.current_setpoint
- @property
- def min_temp(self) -> float:
- """Return the minimum temperature."""
- return DEFAULT_MIN_TEMP
-
- @property
- def max_temp(self) -> float:
- """Return the maximum temperature."""
- return DEFAULT_MAX_TEMP
-
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the current state of the burner."""
diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py
index 2946aacaa72..4af57e03412 100644
--- a/homeassistant/components/toon/const.py
+++ b/homeassistant/components/toon/const.py
@@ -1,5 +1,5 @@
"""Constants for the Toon integration."""
-from datetime import datetime, timedelta
+from datetime import timedelta
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY,
@@ -23,6 +23,7 @@ from homeassistant.const import (
POWER_WATT,
TEMP_CELSIUS,
)
+from homeassistant.util import dt as dt_util
DOMAIN = "toon"
@@ -51,17 +52,13 @@ BINARY_SENSOR_ENTITIES = {
ATTR_NAME: "Boiler Module Connection",
ATTR_SECTION: "thermostat",
ATTR_MEASUREMENT: "boiler_module_connected",
- ATTR_INVERTED: False,
ATTR_DEVICE_CLASS: DEVICE_CLASS_CONNECTIVITY,
- ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
},
"thermostat_info_burner_info_1": {
ATTR_NAME: "Boiler Heating",
ATTR_SECTION: "thermostat",
ATTR_MEASUREMENT: "heating",
- ATTR_INVERTED: False,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:fire",
ATTR_DEFAULT_ENABLED: False,
},
@@ -69,17 +66,12 @@ BINARY_SENSOR_ENTITIES = {
ATTR_NAME: "Hot Tap Water",
ATTR_SECTION: "thermostat",
ATTR_MEASUREMENT: "hot_tapwater",
- ATTR_INVERTED: False,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:water-pump",
- ATTR_DEFAULT_ENABLED: True,
},
"thermostat_info_burner_info_3": {
ATTR_NAME: "Boiler Preheating",
ATTR_SECTION: "thermostat",
ATTR_MEASUREMENT: "pre_heating",
- ATTR_INVERTED: False,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:fire",
ATTR_DEFAULT_ENABLED: False,
},
@@ -87,25 +79,19 @@ BINARY_SENSOR_ENTITIES = {
ATTR_NAME: "Boiler Burner",
ATTR_SECTION: "thermostat",
ATTR_MEASUREMENT: "burner",
- ATTR_INVERTED: False,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:fire",
- ATTR_DEFAULT_ENABLED: True,
},
"thermostat_info_error_found_255": {
ATTR_NAME: "Boiler Status",
ATTR_SECTION: "thermostat",
ATTR_MEASUREMENT: "error_found",
- ATTR_INVERTED: False,
ATTR_DEVICE_CLASS: DEVICE_CLASS_PROBLEM,
ATTR_ICON: "mdi:alert",
- ATTR_DEFAULT_ENABLED: True,
},
"thermostat_info_ot_communication_error_0": {
ATTR_NAME: "OpenTherm Connection",
ATTR_SECTION: "thermostat",
ATTR_MEASUREMENT: "opentherm_communication_error",
- ATTR_INVERTED: False,
ATTR_DEVICE_CLASS: DEVICE_CLASS_PROBLEM,
ATTR_ICON: "mdi:check-network-outline",
ATTR_DEFAULT_ENABLED: False,
@@ -114,10 +100,7 @@ BINARY_SENSOR_ENTITIES = {
ATTR_NAME: "Thermostat Program Override",
ATTR_SECTION: "thermostat",
ATTR_MEASUREMENT: "program_overridden",
- ATTR_INVERTED: False,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:gesture-tap",
- ATTR_DEFAULT_ENABLED: True,
},
}
@@ -128,76 +111,54 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "current_display_temperature",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
- ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
- ATTR_LAST_RESET: None,
},
"gas_average": {
ATTR_NAME: "Average Gas Usage",
ATTR_SECTION: "gas_usage",
ATTR_MEASUREMENT: "average",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_CM3,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:gas-cylinder",
- ATTR_DEFAULT_ENABLED: True,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"gas_average_daily": {
ATTR_NAME: "Average Daily Gas Usage",
ATTR_SECTION: "gas_usage",
ATTR_MEASUREMENT: "day_average",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:gas-cylinder",
ATTR_DEFAULT_ENABLED: False,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"gas_daily_usage": {
ATTR_NAME: "Gas Usage Today",
ATTR_SECTION: "gas_usage",
ATTR_MEASUREMENT: "day_usage",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:gas-cylinder",
- ATTR_DEFAULT_ENABLED: True,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"gas_daily_cost": {
ATTR_NAME: "Gas Cost Today",
ATTR_SECTION: "gas_usage",
ATTR_MEASUREMENT: "day_cost",
ATTR_UNIT_OF_MEASUREMENT: CURRENCY_EUR,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:gas-cylinder",
- ATTR_DEFAULT_ENABLED: True,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"gas_meter_reading": {
ATTR_NAME: "Gas Meter",
ATTR_SECTION: "gas_usage",
ATTR_MEASUREMENT: "meter",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:gas-cylinder",
- ATTR_DEFAULT_ENABLED: False,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
- ATTR_LAST_RESET: datetime.fromtimestamp(0),
+ ATTR_LAST_RESET: dt_util.utc_from_timestamp(0),
+ ATTR_DEFAULT_ENABLED: False,
},
"gas_value": {
ATTR_NAME: "Current Gas Usage",
ATTR_SECTION: "gas_usage",
ATTR_MEASUREMENT: "current",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_CM3,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:gas-cylinder",
- ATTR_DEFAULT_ENABLED: True,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"power_average": {
ATTR_NAME: "Average Power Usage",
@@ -205,10 +166,7 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "average",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
- ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"power_average_daily": {
ATTR_NAME: "Average Daily Energy Usage",
@@ -216,21 +174,14 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "day_average",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
- ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"power_daily_cost": {
ATTR_NAME: "Energy Cost Today",
ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "day_cost",
ATTR_UNIT_OF_MEASUREMENT: CURRENCY_EUR,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:power-plug",
- ATTR_DEFAULT_ENABLED: True,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"power_daily_value": {
ATTR_NAME: "Energy Usage Today",
@@ -238,10 +189,6 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "day_usage",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
- ATTR_ICON: None,
- ATTR_DEFAULT_ENABLED: True,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"power_meter_reading": {
ATTR_NAME: "Electricity Meter Feed IN Tariff 1",
@@ -249,10 +196,9 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "meter_high",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
- ATTR_ICON: None,
- ATTR_DEFAULT_ENABLED: False,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
- ATTR_LAST_RESET: datetime.fromtimestamp(0),
+ ATTR_LAST_RESET: dt_util.utc_from_timestamp(0),
+ ATTR_DEFAULT_ENABLED: False,
},
"power_meter_reading_low": {
ATTR_NAME: "Electricity Meter Feed IN Tariff 2",
@@ -260,10 +206,9 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "meter_low",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
- ATTR_ICON: None,
- ATTR_DEFAULT_ENABLED: False,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
- ATTR_LAST_RESET: datetime.fromtimestamp(0),
+ ATTR_LAST_RESET: dt_util.utc_from_timestamp(0),
+ ATTR_DEFAULT_ENABLED: False,
},
"power_value": {
ATTR_NAME: "Current Power Usage",
@@ -271,10 +216,7 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "current",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
- ATTR_ICON: None,
- ATTR_DEFAULT_ENABLED: True,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
- ATTR_LAST_RESET: None,
},
"solar_meter_reading_produced": {
ATTR_NAME: "Electricity Meter Feed OUT Tariff 1",
@@ -282,10 +224,9 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "meter_produced_high",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
- ATTR_ICON: None,
- ATTR_DEFAULT_ENABLED: False,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
- ATTR_LAST_RESET: datetime.fromtimestamp(0),
+ ATTR_LAST_RESET: dt_util.utc_from_timestamp(0),
+ ATTR_DEFAULT_ENABLED: False,
},
"solar_meter_reading_low_produced": {
ATTR_NAME: "Electricity Meter Feed OUT Tariff 2",
@@ -293,10 +234,9 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "meter_produced_low",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
- ATTR_ICON: None,
- ATTR_DEFAULT_ENABLED: False,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
- ATTR_LAST_RESET: datetime.fromtimestamp(0),
+ ATTR_LAST_RESET: dt_util.utc_from_timestamp(0),
+ ATTR_DEFAULT_ENABLED: False,
},
"solar_value": {
ATTR_NAME: "Current Solar Power Production",
@@ -304,10 +244,7 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "current_solar",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
- ATTR_ICON: None,
- ATTR_DEFAULT_ENABLED: True,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
- ATTR_LAST_RESET: None,
},
"solar_maximum": {
ATTR_NAME: "Max Solar Power Production Today",
@@ -315,10 +252,6 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "day_max_solar",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
- ATTR_ICON: None,
- ATTR_DEFAULT_ENABLED: True,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"solar_produced": {
ATTR_NAME: "Solar Power Production to Grid",
@@ -326,10 +259,7 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "current_produced",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
- ATTR_ICON: None,
- ATTR_DEFAULT_ENABLED: True,
ATTR_STATE_CLASS: ATTR_MEASUREMENT,
- ATTR_LAST_RESET: None,
},
"power_usage_day_produced_solar": {
ATTR_NAME: "Solar Energy Produced Today",
@@ -337,10 +267,6 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "day_produced_solar",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
- ATTR_ICON: None,
- ATTR_DEFAULT_ENABLED: True,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"power_usage_day_to_grid_usage": {
ATTR_NAME: "Energy Produced To Grid Today",
@@ -348,10 +274,7 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "day_to_grid_usage",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
- ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"power_usage_day_from_grid_usage": {
ATTR_NAME: "Energy Usage From Grid Today",
@@ -359,10 +282,7 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "day_from_grid_usage",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
- ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"solar_average_produced": {
ATTR_NAME: "Average Solar Power Production to Grid",
@@ -370,98 +290,75 @@ SENSOR_ENTITIES = {
ATTR_MEASUREMENT: "average_produced",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
- ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"thermostat_info_current_modulation_level": {
ATTR_NAME: "Boiler Modulation Level",
ATTR_SECTION: "thermostat",
ATTR_MEASUREMENT: "current_modulation_level",
ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:percent",
ATTR_DEFAULT_ENABLED: False,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
- ATTR_LAST_RESET: None,
},
"power_usage_current_covered_by_solar": {
ATTR_NAME: "Current Power Usage Covered By Solar",
ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "current_covered_by_solar",
ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:solar-power",
- ATTR_DEFAULT_ENABLED: True,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
- ATTR_LAST_RESET: None,
},
"water_average": {
ATTR_NAME: "Average Water Usage",
ATTR_SECTION: "water_usage",
ATTR_MEASUREMENT: "average",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_LMIN,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:water",
ATTR_DEFAULT_ENABLED: False,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"water_average_daily": {
ATTR_NAME: "Average Daily Water Usage",
ATTR_SECTION: "water_usage",
ATTR_MEASUREMENT: "day_average",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:water",
ATTR_DEFAULT_ENABLED: False,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"water_daily_usage": {
ATTR_NAME: "Water Usage Today",
ATTR_SECTION: "water_usage",
ATTR_MEASUREMENT: "day_usage",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:water",
ATTR_DEFAULT_ENABLED: False,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
"water_meter_reading": {
ATTR_NAME: "Water Meter",
ATTR_SECTION: "water_usage",
ATTR_MEASUREMENT: "meter",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:water",
ATTR_DEFAULT_ENABLED: False,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
- ATTR_LAST_RESET: datetime.fromtimestamp(0),
+ ATTR_LAST_RESET: dt_util.utc_from_timestamp(0),
},
"water_value": {
ATTR_NAME: "Current Water Usage",
ATTR_SECTION: "water_usage",
ATTR_MEASUREMENT: "current",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_LMIN,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:water-pump",
ATTR_DEFAULT_ENABLED: False,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
- ATTR_LAST_RESET: None,
},
"water_daily_cost": {
ATTR_NAME: "Water Cost Today",
ATTR_SECTION: "water_usage",
ATTR_MEASUREMENT: "day_cost",
ATTR_UNIT_OF_MEASUREMENT: CURRENCY_EUR,
- ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:water-pump",
ATTR_DEFAULT_ENABLED: False,
- ATTR_STATE_CLASS: None,
- ATTR_LAST_RESET: None,
},
}
@@ -470,16 +367,12 @@ SWITCH_ENTITIES = {
ATTR_NAME: "Holiday Mode",
ATTR_SECTION: "thermostat",
ATTR_MEASUREMENT: "holiday_mode",
- ATTR_INVERTED: False,
ATTR_ICON: "mdi:airport",
- ATTR_DEFAULT_ENABLED: True,
},
"thermostat_program": {
ATTR_NAME: "Thermostat Program",
ATTR_SECTION: "thermostat",
ATTR_MEASUREMENT: "program",
- ATTR_INVERTED: False,
ATTR_ICON: "mdi:calendar-clock",
- ATTR_DEFAULT_ENABLED: True,
},
}
diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py
index 18b44db45a8..7fb45af4d53 100644
--- a/homeassistant/components/toon/models.py
+++ b/homeassistant/components/toon/models.py
@@ -11,35 +11,7 @@ from .coordinator import ToonDataUpdateCoordinator
class ToonEntity(CoordinatorEntity):
"""Defines a base Toon entity."""
- def __init__(
- self,
- coordinator: ToonDataUpdateCoordinator,
- *,
- name: str,
- icon: str,
- enabled_default: bool = True,
- ) -> None:
- """Initialize the Toon entity."""
- super().__init__(coordinator)
- self._enabled_default = enabled_default
- self._icon = icon
- self._name = name
- self._state = None
-
- @property
- def name(self) -> str:
- """Return the name of the entity."""
- return self._name
-
- @property
- def icon(self) -> str | None:
- """Return the mdi icon of the entity."""
- return self._icon
-
- @property
- def entity_registry_enabled_default(self) -> bool:
- """Return if the entity should be enabled when first added to the entity registry."""
- return self._enabled_default
+ coordinator: ToonDataUpdateCoordinator
class ToonDisplayDeviceEntity(ToonEntity):
diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py
index 0e269c0bff3..90f74ceae87 100644
--- a/homeassistant/components/toon/sensor.py
+++ b/homeassistant/components/toon/sensor.py
@@ -120,26 +120,23 @@ class ToonSensor(ToonEntity, SensorEntity):
def __init__(self, coordinator: ToonDataUpdateCoordinator, *, key: str) -> None:
"""Initialize the Toon sensor."""
self.key = key
+ super().__init__(coordinator)
- super().__init__(
- coordinator,
- enabled_default=SENSOR_ENTITIES[key][ATTR_DEFAULT_ENABLED],
- icon=SENSOR_ENTITIES[key][ATTR_ICON],
- name=SENSOR_ENTITIES[key][ATTR_NAME],
+ sensor = SENSOR_ENTITIES[key]
+ self._attr_entity_registry_enabled_default = sensor.get(
+ ATTR_DEFAULT_ENABLED, True
+ )
+ self._attr_icon = sensor.get(ATTR_ICON)
+ self._attr_last_reset = sensor.get(ATTR_LAST_RESET)
+ self._attr_name = sensor[ATTR_NAME]
+ self._attr_state_class = sensor.get(ATTR_STATE_CLASS)
+ self._attr_unit_of_measurement = sensor[ATTR_UNIT_OF_MEASUREMENT]
+ self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS)
+ self._attr_unique_id = (
+ # This unique ID is a bit ugly and contains unneeded information.
+ # It is here for legacy / backward compatible reasons.
+ f"{DOMAIN}_{coordinator.data.agreement.agreement_id}_sensor_{key}"
)
-
- self._attr_last_reset = SENSOR_ENTITIES[key][ATTR_LAST_RESET]
- self._attr_state_class = SENSOR_ENTITIES[key][ATTR_STATE_CLASS]
- self._attr_unit_of_measurement = SENSOR_ENTITIES[key][ATTR_UNIT_OF_MEASUREMENT]
- self._sttr_device_class = SENSOR_ENTITIES[key][ATTR_DEVICE_CLASS]
-
- @property
- def unique_id(self) -> str:
- """Return the unique ID for this sensor."""
- agreement_id = self.coordinator.data.agreement.agreement_id
- # This unique ID is a bit ugly and contains unneeded information.
- # It is here for legacy / backward compatible reasons.
- return f"{DOMAIN}_{agreement_id}_sensor_{self.key}"
@property
def state(self) -> str | None:
diff --git a/homeassistant/components/toon/switch.py b/homeassistant/components/toon/switch.py
index b830f53179e..06ca9c6631b 100644
--- a/homeassistant/components/toon/switch.py
+++ b/homeassistant/components/toon/switch.py
@@ -13,9 +13,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import (
- ATTR_DEFAULT_ENABLED,
ATTR_ICON,
- ATTR_INVERTED,
ATTR_MEASUREMENT,
ATTR_NAME,
ATTR_SECTION,
@@ -44,19 +42,12 @@ class ToonSwitch(ToonEntity, SwitchEntity):
def __init__(self, coordinator: ToonDataUpdateCoordinator, *, key: str) -> None:
"""Initialize the Toon switch."""
self.key = key
+ super().__init__(coordinator)
- super().__init__(
- coordinator,
- enabled_default=SWITCH_ENTITIES[key][ATTR_DEFAULT_ENABLED],
- icon=SWITCH_ENTITIES[key][ATTR_ICON],
- name=SWITCH_ENTITIES[key][ATTR_NAME],
- )
-
- @property
- def unique_id(self) -> str:
- """Return the unique ID for this binary sensor."""
- agreement_id = self.coordinator.data.agreement.agreement_id
- return f"{agreement_id}_{self.key}"
+ switch = SWITCH_ENTITIES[key]
+ self._attr_icon = switch[ATTR_ICON]
+ self._attr_name = switch[ATTR_NAME]
+ self._attr_unique_id = f"{coordinator.data.agreement.agreement_id}_{key}"
@property
def is_on(self) -> bool:
@@ -64,12 +55,7 @@ class ToonSwitch(ToonEntity, SwitchEntity):
section = getattr(
self.coordinator.data, SWITCH_ENTITIES[self.key][ATTR_SECTION]
)
- value = getattr(section, SWITCH_ENTITIES[self.key][ATTR_MEASUREMENT])
-
- if SWITCH_ENTITIES[self.key][ATTR_INVERTED]:
- return not value
-
- return value
+ return getattr(section, SWITCH_ENTITIES[self.key][ATTR_MEASUREMENT])
class ToonProgramSwitch(ToonSwitch, ToonDisplayDeviceEntity):
diff --git a/homeassistant/components/toon/translations/de.json b/homeassistant/components/toon/translations/de.json
index daeead855c3..c76bab5ef91 100644
--- a/homeassistant/components/toon/translations/de.json
+++ b/homeassistant/components/toon/translations/de.json
@@ -15,6 +15,9 @@
},
"description": "W\u00e4hlen Sie die Vereinbarungsadresse aus, die du hinzuf\u00fcgen m\u00f6chtest.",
"title": "W\u00e4hle deine Vereinbarung"
+ },
+ "pick_implementation": {
+ "title": "W\u00e4hlen Sie Ihren Mandanten f\u00fcr die Authentifizierung aus"
}
}
}
diff --git a/homeassistant/components/toon/translations/he.json b/homeassistant/components/toon/translations/he.json
new file mode 100644
index 00000000000..431a7b32509
--- /dev/null
+++ b/homeassistant/components/toon/translations/he.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+ "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py
index 7026abc34f9..2183448eed7 100644
--- a/homeassistant/components/totalconnect/__init__.py
+++ b/homeassistant/components/totalconnect/__init__.py
@@ -14,7 +14,7 @@ PLATFORMS = ["alarm_control_panel", "binary_sensor"]
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up upon config entry in user interface."""
conf = entry.data
username = conf[CONF_USERNAME]
diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json
index ce055082a21..cbe1d4e449c 100644
--- a/homeassistant/components/totalconnect/translations/ca.json
+++ b/homeassistant/components/totalconnect/translations/ca.json
@@ -11,9 +11,10 @@
"step": {
"locations": {
"data": {
- "location": "Ubicaci\u00f3"
+ "location": "Ubicaci\u00f3",
+ "usercode": "Codi d'usuari"
},
- "description": "Introdueix el codi d'usuari de l'usuari en aquesta ubicaci\u00f3",
+ "description": "Introdueix el codi de l'usuari en la ubicaci\u00f3 {location_id}",
"title": "Codis d'usuari d'ubicaci\u00f3"
},
"reauth_confirm": {
diff --git a/homeassistant/components/totalconnect/translations/de.json b/homeassistant/components/totalconnect/translations/de.json
index 2f484f2a8ed..b6543752043 100644
--- a/homeassistant/components/totalconnect/translations/de.json
+++ b/homeassistant/components/totalconnect/translations/de.json
@@ -11,9 +11,10 @@
"step": {
"locations": {
"data": {
- "location": "Standort"
+ "location": "Standort",
+ "usercode": "Benutzercode"
},
- "description": "Geben Sie den Benutzercode f\u00fcr diesen Benutzer an dieser Stelle ein",
+ "description": "Geben Sie den Benutzercode f\u00fcr den Benutzer {location_id} an dieser Stelle ein",
"title": "Standort-Benutzercodes"
},
"reauth_confirm": {
diff --git a/homeassistant/components/totalconnect/translations/en.json b/homeassistant/components/totalconnect/translations/en.json
index 02ea1bfccbd..05f394fbb31 100644
--- a/homeassistant/components/totalconnect/translations/en.json
+++ b/homeassistant/components/totalconnect/translations/en.json
@@ -11,6 +11,7 @@
"step": {
"locations": {
"data": {
+ "location": "Location",
"usercode": "Usercode"
},
"description": "Enter the usercode for this user at location {location_id}",
diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json
index 07837760a44..c4923884c43 100644
--- a/homeassistant/components/totalconnect/translations/es.json
+++ b/homeassistant/components/totalconnect/translations/es.json
@@ -11,7 +11,8 @@
"step": {
"locations": {
"data": {
- "location": "Localizaci\u00f3n"
+ "location": "Localizaci\u00f3n",
+ "usercode": "Codigo de usuario"
},
"description": "Ingrese el c\u00f3digo de usuario para este usuario en esta ubicaci\u00f3n",
"title": "C\u00f3digos de usuario de ubicaci\u00f3n"
diff --git a/homeassistant/components/totalconnect/translations/et.json b/homeassistant/components/totalconnect/translations/et.json
index 3f1a15fe139..a4110f9bf0f 100644
--- a/homeassistant/components/totalconnect/translations/et.json
+++ b/homeassistant/components/totalconnect/translations/et.json
@@ -11,9 +11,10 @@
"step": {
"locations": {
"data": {
- "location": "Asukoht"
+ "location": "Asukoht",
+ "usercode": "Kasutajakood"
},
- "description": "Sisesta selle kasutaja kood selles asukohas",
+ "description": "Sisesta kasutaja kood asukohale {location_id}",
"title": "Asukoha kasutajakoodid"
},
"reauth_confirm": {
diff --git a/homeassistant/components/totalconnect/translations/he.json b/homeassistant/components/totalconnect/translations/he.json
index ed07845a182..712c12a1062 100644
--- a/homeassistant/components/totalconnect/translations/he.json
+++ b/homeassistant/components/totalconnect/translations/he.json
@@ -1,11 +1,22 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
"step": {
"locations": {
"data": {
- "location": "\u05de\u05d9\u05e7\u05d5\u05dd"
+ "location": "\u05de\u05d9\u05e7\u05d5\u05dd",
+ "usercode": "\u05e7\u05d5\u05d3 \u05de\u05e9\u05ea\u05de\u05e9"
}
},
+ "reauth_confirm": {
+ "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/totalconnect/translations/it.json b/homeassistant/components/totalconnect/translations/it.json
index 18ecf648310..dfc480ab961 100644
--- a/homeassistant/components/totalconnect/translations/it.json
+++ b/homeassistant/components/totalconnect/translations/it.json
@@ -11,9 +11,10 @@
"step": {
"locations": {
"data": {
- "location": "Posizione"
+ "location": "Posizione",
+ "usercode": "Codice utente"
},
- "description": "Immettere il codice utente per questo utente in questa posizione",
+ "description": "Inserisci il codice utente per questo utente nella posizione {location_id}",
"title": "Codici utente posizione"
},
"reauth_confirm": {
diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json
index de20d40bee6..0ec7bb52d88 100644
--- a/homeassistant/components/totalconnect/translations/nl.json
+++ b/homeassistant/components/totalconnect/translations/nl.json
@@ -11,9 +11,10 @@
"step": {
"locations": {
"data": {
- "location": "Locatie"
+ "location": "Locatie",
+ "usercode": "Gebruikerscode"
},
- "description": "Voer de gebruikerscode voor deze gebruiker op deze locatie in",
+ "description": "Voer de gebruikerscode voor deze gebruiker in op locatie {location_id}",
"title": "Locatie gebruikerscodes"
},
"reauth_confirm": {
diff --git a/homeassistant/components/totalconnect/translations/no.json b/homeassistant/components/totalconnect/translations/no.json
index 9c98d6ad1e7..839d901047b 100644
--- a/homeassistant/components/totalconnect/translations/no.json
+++ b/homeassistant/components/totalconnect/translations/no.json
@@ -11,9 +11,10 @@
"step": {
"locations": {
"data": {
- "location": "Plassering"
+ "location": "Plassering",
+ "usercode": "Brukerkode"
},
- "description": "Angi brukerkoden for denne brukeren p\u00e5 denne plasseringen",
+ "description": "Angi brukerkoden for denne brukeren p\u00e5 plasseringen {location_id}",
"title": "Brukerkoder for plassering"
},
"reauth_confirm": {
diff --git a/homeassistant/components/totalconnect/translations/pl.json b/homeassistant/components/totalconnect/translations/pl.json
index ff2ca2351e6..03452569c28 100644
--- a/homeassistant/components/totalconnect/translations/pl.json
+++ b/homeassistant/components/totalconnect/translations/pl.json
@@ -11,9 +11,10 @@
"step": {
"locations": {
"data": {
- "location": "Lokalizacja"
+ "location": "Lokalizacja",
+ "usercode": "Kod u\u017cytkownika"
},
- "description": "Wprowad\u017a kod u\u017cytkownika dla u\u017cytkownika w tej lokalizacji",
+ "description": "Wprowad\u017a kod u\u017cytkownika dla u\u017cytkownika w lokalizacji {location_id}",
"title": "Kody lokalizacji u\u017cytkownika"
},
"reauth_confirm": {
diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json
index a4e48ca01d4..268f620c238 100644
--- a/homeassistant/components/totalconnect/translations/ru.json
+++ b/homeassistant/components/totalconnect/translations/ru.json
@@ -11,9 +11,10 @@
"step": {
"locations": {
"data": {
- "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435"
+ "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435",
+ "usercode": "\u041a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
- "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u044d\u0442\u043e\u043c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438.",
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438 {location_id}.",
"title": "\u041a\u043e\u0434\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
},
"reauth_confirm": {
diff --git a/homeassistant/components/totalconnect/translations/zh-Hant.json b/homeassistant/components/totalconnect/translations/zh-Hant.json
index 96921baf007..eb739cb5e38 100644
--- a/homeassistant/components/totalconnect/translations/zh-Hant.json
+++ b/homeassistant/components/totalconnect/translations/zh-Hant.json
@@ -11,9 +11,10 @@
"step": {
"locations": {
"data": {
- "location": "\u5ea7\u6a19"
+ "location": "\u5ea7\u6a19",
+ "usercode": "\u4f7f\u7528\u8005\u4ee3\u78bc"
},
- "description": "\u8f38\u5165\u4f7f\u7528\u8005\u65bc\u6b64\u5ea7\u6a19\u4e4b\u4f7f\u7528\u8005\u4ee3\u78bc",
+ "description": "\u8f38\u5165\u4f7f\u7528\u8005\u65bc\u6b64\u5ea7\u6a19 {location_id} \u4e4b\u4f7f\u7528\u8005\u4ee3\u78bc",
"title": "\u5ea7\u6a19\u4f7f\u7528\u8005\u4ee3\u78bc"
},
"reauth_confirm": {
diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py
index 69241f1cb44..1f843d364d8 100644
--- a/homeassistant/components/tplink/__init__.py
+++ b/homeassistant/components/tplink/__init__.py
@@ -7,6 +7,7 @@ from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
@@ -72,10 +73,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up TPLink from a config entry."""
config_data = hass.data[DOMAIN].get(ATTR_CONFIG)
+ device_registry = dr.async_get(hass)
+ tplink_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
+ device_count = len(tplink_devices)
+
# These will contain the initialized devices
lights = hass.data[DOMAIN][CONF_LIGHT] = []
switches = hass.data[DOMAIN][CONF_SWITCH] = []
@@ -90,7 +95,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
# Add discovered devices
if config_data is None or config_data[CONF_DISCOVERY]:
- discovered_devices = await async_discover_devices(hass, static_devices)
+ discovered_devices = await async_discover_devices(
+ hass, static_devices, device_count
+ )
lights.extend(discovered_devices.lights)
switches.extend(discovered_devices.switches)
@@ -101,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
"Got %s lights: %s", len(lights), ", ".join(d.host for d in lights)
)
- hass.async_create_task(forward_setup(config_entry, "light"))
+ hass.async_create_task(forward_setup(entry, "light"))
if switches:
_LOGGER.debug(
@@ -110,7 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
", ".join(d.host for d in switches),
)
- hass.async_create_task(forward_setup(config_entry, "switch"))
+ hass.async_create_task(forward_setup(entry, "switch"))
return True
diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py
index 8b1ee4a44b1..3096281776a 100644
--- a/homeassistant/components/tplink/common.py
+++ b/homeassistant/components/tplink/common.py
@@ -26,6 +26,7 @@ CONF_DISCOVERY = "discovery"
CONF_LIGHT = "light"
CONF_STRIP = "strip"
CONF_SWITCH = "switch"
+MAX_DISCOVERY_RETRIES = 4
class SmartDevices:
@@ -67,12 +68,9 @@ async def async_get_discoverable_devices(hass: HomeAssistant) -> dict[str, Smart
async def async_discover_devices(
- hass: HomeAssistant, existing_devices: SmartDevices
+ hass: HomeAssistant, existing_devices: SmartDevices, target_device_count: int
) -> SmartDevices:
"""Get devices through discovery."""
- _LOGGER.debug("Discovering devices")
- devices = await async_get_discoverable_devices(hass)
- _LOGGER.info("Discovered %s TP-Link smart home device(s)", len(devices))
lights = []
switches = []
@@ -100,6 +98,33 @@ async def async_discover_devices(
else:
_LOGGER.error("Unknown smart device type: %s", type(dev))
+ devices = {}
+ for attempt in range(1, MAX_DISCOVERY_RETRIES + 1):
+ _LOGGER.debug(
+ "Discovering tplink devices, attempt %s of %s",
+ attempt,
+ MAX_DISCOVERY_RETRIES,
+ )
+ discovered_devices = await async_get_discoverable_devices(hass)
+ _LOGGER.info(
+ "Discovered %s TP-Link of expected %s smart home device(s)",
+ len(discovered_devices),
+ target_device_count,
+ )
+ for device_ip in discovered_devices:
+ devices[device_ip] = discovered_devices[device_ip]
+
+ if len(discovered_devices) >= target_device_count:
+ _LOGGER.info(
+ "Discovered at least as many devices on the network as exist in our device registry, no need to retry"
+ )
+ break
+
+ _LOGGER.info(
+ "Found %s unique TP-Link smart home device(s) after %s discovery attempts",
+ len(devices),
+ attempt,
+ )
await hass.async_add_executor_job(process_devices)
return SmartDevices(lights, switches)
diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py
index e5217cbc143..6d497812261 100644
--- a/homeassistant/components/tplink/light.py
+++ b/homeassistant/components/tplink/light.py
@@ -432,7 +432,10 @@ class TPLinkSmartBulb(LightEntity):
self._is_setting_light_state = False
if LIGHT_STATE_ERROR_MSG in light_state_params:
raise HomeAssistantError(light_state_params[LIGHT_STATE_ERROR_MSG])
- self._light_state = self._light_state_from_params(light_state_params)
+ # Some devices do not report the new state in their responses, so we skip
+ # set here and wait for the next poll to update the values. See #47600
+ if LIGHT_STATE_ON_OFF in light_state_params:
+ self._light_state = self._light_state_from_params(light_state_params)
return
except (SmartDeviceException, OSError):
pass
diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py
index b4d1f919238..661cb190877 100644
--- a/homeassistant/components/traccar/device_tracker.py
+++ b/homeassistant/components/traccar/device_tracker.py
@@ -11,6 +11,7 @@ from homeassistant.components.device_tracker import (
SOURCE_TYPE_GPS,
)
from homeassistant.components.device_tracker.config_entry import TrackerEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_EVENT,
CONF_HOST,
@@ -116,7 +117,9 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
)
-async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities
+):
"""Configure a dispatcher connection based on a config entry."""
@callback
diff --git a/homeassistant/components/traccar/translations/he.json b/homeassistant/components/traccar/translations/he.json
new file mode 100644
index 00000000000..ebee9aee976
--- /dev/null
+++ b/homeassistant/components/traccar/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "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.",
+ "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py
index bf8fa00bbc8..cf39d3d6c05 100644
--- a/homeassistant/components/tradfri/__init__.py
+++ b/homeassistant/components/tradfri/__init__.py
@@ -100,7 +100,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Create a gateway."""
# host, identity, key, allow_tradfri_groups
tradfri_data = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {}
diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py
index 455ca69147d..1f028849d32 100644
--- a/homeassistant/components/tradfri/sensor.py
+++ b/homeassistant/components/tradfri/sensor.py
@@ -29,22 +29,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class TradfriSensor(TradfriBaseDevice, SensorEntity):
"""The platform class required by Home Assistant."""
+ _attr_device_class = DEVICE_CLASS_BATTERY
+ _attr_unit_of_measurement = PERCENTAGE
+
def __init__(self, device, api, gateway_id):
"""Initialize the device."""
super().__init__(device, api, gateway_id)
self._unique_id = f"{gateway_id}-{device.id}"
- @property
- def device_class(self):
- """Return the devices' state attributes."""
- return DEVICE_CLASS_BATTERY
-
@property
def state(self):
"""Return the current state of the device."""
return self._device.device_info.battery_level
-
- @property
- def unit_of_measurement(self):
- """Return the unit_of_measurement of the device."""
- return PERCENTAGE
diff --git a/homeassistant/components/tradfri/translations/he.json b/homeassistant/components/tradfri/translations/he.json
index f1731579816..800cb59a744 100644
--- a/homeassistant/components/tradfri/translations/he.json
+++ b/homeassistant/components/tradfri/translations/he.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ "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"
},
"error": {
- "cannot_connect": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05d2\u05e9\u05e8",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
"invalid_key": "\u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05e0\u05db\u05e9\u05dc \u05e2\u05dd \u05d4\u05de\u05e4\u05ea\u05d7 \u05e9\u05e1\u05d5\u05e4\u05e7. \u05d0\u05dd \u05d6\u05d4 \u05e7\u05d5\u05e8\u05d4 \u05e9\u05d5\u05d1, \u05e0\u05e1\u05d4 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05de\u05d2\u05e9\u05e8.",
"timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3"
},
diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py
index 37e3bd52cdc..5e541045266 100644
--- a/homeassistant/components/trafikverket_train/sensor.py
+++ b/homeassistant/components/trafikverket_train/sensor.py
@@ -118,6 +118,8 @@ def next_departuredate(departure):
class TrainSensor(SensorEntity):
"""Contains data about a train depature."""
+ _attr_device_class = DEVICE_CLASS_TIMESTAMP
+
def __init__(self, train_api, name, from_station, to_station, weekday, time):
"""Initialize the sensor."""
self._train_api = train_api
@@ -176,11 +178,6 @@ class TrainSensor(SensorEntity):
ATTR_DEVIATIONS: deviations,
}
- @property
- def device_class(self):
- """Return the device class."""
- return DEVICE_CLASS_TIMESTAMP
-
@property
def name(self):
"""Return the name of the sensor."""
diff --git a/homeassistant/components/transmission/translations/he.json b/homeassistant/components/transmission/translations/he.json
index 6f4191da70d..6f8286290d4 100644
--- a/homeassistant/components/transmission/translations/he.json
+++ b/homeassistant/components/transmission/translations/he.json
@@ -1,8 +1,19 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
"step": {
"user": {
"data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "name": "\u05e9\u05dd",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py
index 86ba0e12c61..595350324f9 100644
--- a/homeassistant/components/tuya/__init__.py
+++ b/homeassistant/components/tuya/__init__.py
@@ -93,7 +93,7 @@ def _update_query_interval(hass, interval):
_LOGGER.warning(ex)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Tuya platform."""
tuya = TuyaApi()
diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json
index 8aa7c08f352..289d2661485 100644
--- a/homeassistant/components/tuya/translations/de.json
+++ b/homeassistant/components/tuya/translations/de.json
@@ -40,12 +40,14 @@
"max_temp": "Maximale Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)",
"min_kelvin": "Minimale unterst\u00fctzte Farbtemperatur in Kelvin",
"min_temp": "Minimal Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)",
+ "set_temp_divided": "Geteilten Temperaturwert f\u00fcr Solltemperaturbefehl verwenden",
"support_color": "Farbunterst\u00fctzung erzwingen",
"temp_divider": "Teiler f\u00fcr Temperaturwerte (0 = Standard verwenden)",
+ "temp_step_override": "Zieltemperaturschritt",
"tuya_max_coltemp": "Vom Ger\u00e4t gemeldete maximale Farbtemperatur",
"unit_of_measurement": "Vom Ger\u00e4t verwendete Temperatureinheit"
},
- "description": "Optionen zur Anpassung der angezeigten Informationen f\u00fcr das Ger\u00e4t `{Ger\u00e4tename}` konfigurieren",
+ "description": "Optionen zur Anpassung der angezeigten Informationen f\u00fcr das Ger\u00e4t `{device_name}` vom Typ: {device_type}konfigurieren",
"title": "Tuya-Ger\u00e4t konfigurieren"
},
"init": {
diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json
index 98dafb882c7..0a05bec6b21 100644
--- a/homeassistant/components/tuya/translations/he.json
+++ b/homeassistant/components/tuya/translations/he.json
@@ -1,5 +1,14 @@
{
"config": {
+ "abort": {
+ "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",
+ "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"
+ },
+ "flow_title": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d8\u05d5\u05d9\u05d4",
"step": {
"user": {
"data": {
@@ -7,8 +16,15 @@
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"platform": "\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05e9\u05d1\u05d4 \u05e8\u05e9\u05d5\u05dd \u05d7\u05e9\u05d1\u05d5\u05e0\u05da",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
- }
+ },
+ "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05d8\u05d5\u05d9\u05d4 \u05e9\u05dc\u05da.",
+ "title": "Tuya"
}
}
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json
index f1049d6882e..ed0488f524d 100644
--- a/homeassistant/components/tuya/translations/nl.json
+++ b/homeassistant/components/tuya/translations/nl.json
@@ -36,7 +36,7 @@
"data": {
"brightness_range_mode": "Helderheidsbereik gebruikt door apparaat",
"curr_temp_divider": "Huidige temperatuurwaarde deler (0 = standaardwaarde)",
- "max_kelvin": "Max ondersteunde kleurtemperatuur in kelvin",
+ "max_kelvin": "Max kleurtemperatuur in kelvin",
"max_temp": "Maximale doeltemperatuur (gebruik min en max = 0 voor standaardwaarde)",
"min_kelvin": "Minimaal ondersteunde kleurtemperatuur in kelvin",
"min_temp": "Min. gewenste temperatuur (gebruik min en max = 0 voor standaard)",
diff --git a/homeassistant/components/twentemilieu/translations/he.json b/homeassistant/components/twentemilieu/translations/he.json
new file mode 100644
index 00000000000..6faac53655f
--- /dev/null
+++ b/homeassistant/components/twentemilieu/translations/he.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/translations/he.json b/homeassistant/components/twilio/translations/he.json
new file mode 100644
index 00000000000..7e155c6bdd7
--- /dev/null
+++ b/homeassistant/components/twilio/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.",
+ "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook."
+ },
+ "step": {
+ "user": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py
index 24c714dc437..3a9a2a8faa2 100644
--- a/homeassistant/components/twinkly/__init__.py
+++ b/homeassistant/components/twinkly/__init__.py
@@ -11,29 +11,29 @@ from .const import CONF_ENTRY_HOST, CONF_ENTRY_ID, DOMAIN
PLATFORMS = ["light"]
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up entries from config flow."""
# We setup the client here so if at some point we add any other entity for this device,
# we will be able to properly share the connection.
- uuid = config_entry.data[CONF_ENTRY_ID]
- host = config_entry.data[CONF_ENTRY_HOST]
+ uuid = entry.data[CONF_ENTRY_ID]
+ host = entry.data[CONF_ENTRY_HOST]
hass.data.setdefault(DOMAIN, {})[uuid] = twinkly_client.TwinklyClient(
host, async_get_clientsession(hass)
)
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
-async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Remove a twinkly entry."""
# For now light entries don't have unload method, so we don't have to async_forward_entry_unload
# However we still have to cleanup the shared client!
- uuid = config_entry.data[CONF_ENTRY_ID]
+ uuid = entry.data[CONF_ENTRY_ID]
hass.data[DOMAIN].pop(uuid)
return True
diff --git a/homeassistant/components/twinkly/translations/he.json b/homeassistant/components/twinkly/translations/he.json
new file mode 100644
index 00000000000..db9e846ce56
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/he.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "\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 (\u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP) \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df \u05d4\u05de\u05e0\u05e6\u05e0\u05e5 \u05e9\u05dc\u05da"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py
index a0448230dd1..41549c202b3 100644
--- a/homeassistant/components/uk_transport/sensor.py
+++ b/homeassistant/components/uk_transport/sensor.py
@@ -92,7 +92,8 @@ class UkTransportSensor(SensorEntity):
"""
TRANSPORT_API_URL_BASE = "https://transportapi.com/v3/uk/"
- ICON = "mdi:train"
+ _attr_icon = "mdi:train"
+ _attr_unit_of_measurement = TIME_MINUTES
def __init__(self, name, api_app_id, api_app_key, url):
"""Initialize the sensor."""
@@ -113,16 +114,6 @@ class UkTransportSensor(SensorEntity):
"""Return the state of the sensor."""
return self._state
- @property
- def unit_of_measurement(self):
- """Return the unit this state is expressed in."""
- return TIME_MINUTES
-
- @property
- def icon(self):
- """Icon to use in the frontend, if any."""
- return self.ICON
-
def _do_api_request(self, params):
"""Perform an API request."""
request_params = dict(
@@ -144,7 +135,7 @@ class UkTransportSensor(SensorEntity):
class UkTransportLiveBusTimeSensor(UkTransportSensor):
"""Live bus time sensor from UK transportapi.com."""
- ICON = "mdi:bus"
+ _attr_icon = "mdi:bus"
def __init__(self, api_app_id, api_app_key, stop_atcocode, bus_direction, interval):
"""Construct a live bus time sensor."""
@@ -206,7 +197,7 @@ class UkTransportLiveBusTimeSensor(UkTransportSensor):
class UkTransportLiveTrainTimeSensor(UkTransportSensor):
"""Live train time sensor from UK transportapi.com."""
- ICON = "mdi:train"
+ _attr_icon = "mdi:train"
def __init__(self, api_app_id, api_app_key, station_code, calling_at, interval):
"""Construct a live bus time sensor."""
diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py
index c8238602856..8d34d3cabd7 100644
--- a/homeassistant/components/unifi/sensor.py
+++ b/homeassistant/components/unifi/sensor.py
@@ -35,7 +35,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
) -> None:
"""Update the values of the controller."""
if controller.option_allow_bandwidth_sensors:
- add_bandwith_entities(controller, async_add_entities, clients)
+ add_bandwidth_entities(controller, async_add_entities, clients)
if controller.option_allow_uptime_sensors:
add_uptime_entities(controller, async_add_entities, clients)
@@ -49,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
-def add_bandwith_entities(controller, async_add_entities, clients):
+def add_bandwidth_entities(controller, async_add_entities, clients):
"""Add new sensor entities from the controller."""
sensors = []
@@ -86,16 +86,13 @@ class UniFiBandwidthSensor(UniFiClient, SensorEntity):
DOMAIN = DOMAIN
+ _attr_unit_of_measurement = DATA_MEGABYTES
+
@property
def name(self) -> str:
"""Return the name of the client."""
return f"{super().name} {self.TYPE.upper()}"
- @property
- def unit_of_measurement(self) -> str:
- """Return the unit of measurement of this entity."""
- return DATA_MEGABYTES
-
async def options_updated(self) -> None:
"""Config entry options are updated, remove entity if option is disabled."""
if not self.controller.option_allow_bandwidth_sensors:
@@ -134,10 +131,7 @@ class UniFiUpTimeSensor(UniFiClient, SensorEntity):
DOMAIN = DOMAIN
TYPE = UPTIME_SENSOR
- @property
- def device_class(self) -> str:
- """Return device class."""
- return DEVICE_CLASS_TIMESTAMP
+ _attr_device_class = DEVICE_CLASS_TIMESTAMP
@property
def name(self) -> str:
diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json
index 4c34101a7ce..f1f2fdd3627 100644
--- a/homeassistant/components/unifi/translations/de.json
+++ b/homeassistant/components/unifi/translations/de.json
@@ -10,7 +10,7 @@
"service_unavailable": "Verbindung fehlgeschlagen",
"unknown_client_mac": "Unter dieser MAC-Adresse ist kein Client verf\u00fcgbar."
},
- "flow_title": "UniFi Netzwerk {site} ({host})",
+ "flow_title": "{site} ({host})",
"step": {
"user": {
"data": {
@@ -64,7 +64,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Bandbreitennutzungssensoren f\u00fcr Netzwerkclients"
+ "allow_bandwidth_sensors": "Bandbreitennutzungssensoren f\u00fcr Netzwerkclients",
+ "allow_uptime_sensors": "Uptime-Sensoren f\u00fcr Netzwerk-Clients"
},
"description": "Konfigurieren Sie Statistiksensoren",
"title": "UniFi-Optionen 3/3"
diff --git a/homeassistant/components/unifi/translations/he.json b/homeassistant/components/unifi/translations/he.json
index 3007c0e968c..83c34cb9c77 100644
--- a/homeassistant/components/unifi/translations/he.json
+++ b/homeassistant/components/unifi/translations/he.json
@@ -1,9 +1,48 @@
{
"config": {
+ "abort": {
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "faulty_credentials": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "service_unavailable": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{site} ({host})",
"step": {
"user": {
"data": {
- "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05ea\u05d7\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "client_control": {
+ "data": {
+ "block_client": "\u05dc\u05e7\u05d5\u05d7\u05d5\u05ea \u05de\u05d1\u05d5\u05e7\u05e8\u05d9\u05dd \u05e9\u05dc \u05d2\u05d9\u05e9\u05d4 \u05dc\u05e8\u05e9\u05ea"
+ }
+ },
+ "device_tracker": {
+ "data": {
+ "track_clients": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05dc\u05e7\u05d5\u05d7\u05d5\u05ea \u05e8\u05e9\u05ea",
+ "track_devices": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9 \u05e8\u05e9\u05ea (\u05d4\u05ea\u05e7\u05e0\u05d9 Ubiquiti)"
+ }
+ },
+ "simple_options": {
+ "data": {
+ "block_client": "\u05dc\u05e7\u05d5\u05d7\u05d5\u05ea \u05de\u05d1\u05d5\u05e7\u05e8\u05d9\u05dd \u05e9\u05dc \u05d2\u05d9\u05e9\u05d4 \u05dc\u05e8\u05e9\u05ea",
+ "track_clients": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05dc\u05e7\u05d5\u05d7\u05d5\u05ea \u05e8\u05e9\u05ea",
+ "track_devices": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9 \u05e8\u05e9\u05ea (\u05d4\u05ea\u05e7\u05e0\u05d9 Ubiquiti)"
+ }
+ },
+ "statistics_sensors": {
+ "data": {
+ "allow_bandwidth_sensors": "\u05d7\u05d9\u05d9\u05e9\u05e0\u05d9 \u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05e8\u05d5\u05d7\u05d1 \u05e4\u05e1 \u05dc\u05dc\u05e7\u05d5\u05d7\u05d5\u05ea \u05e8\u05e9\u05ea"
}
}
}
diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json
index 4602193850f..745b628b253 100644
--- a/homeassistant/components/unifi/translations/hu.json
+++ b/homeassistant/components/unifi/translations/hu.json
@@ -8,7 +8,7 @@
"faulty_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"service_unavailable": "Sikertelen csatlakoz\u00e1s"
},
- "flow_title": "UniFi Network {site} ({host})",
+ "flow_title": "{site} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json
index 007da9f80ed..00672b65b4e 100644
--- a/homeassistant/components/unifi/translations/it.json
+++ b/homeassistant/components/unifi/translations/it.json
@@ -48,6 +48,12 @@
"description": "Configurare il tracciamento del dispositivo",
"title": "Opzioni UniFi 1/3"
},
+ "init": {
+ "data": {
+ "one": "Pi\u00f9",
+ "other": "Altri"
+ }
+ },
"simple_options": {
"data": {
"block_client": "Client controllati per l'accesso alla rete",
diff --git a/homeassistant/components/upb/translations/he.json b/homeassistant/components/upb/translations/he.json
index ece7d57a907..e89a02adfa4 100644
--- a/homeassistant/components/upb/translations/he.json
+++ b/homeassistant/components/upb/translations/he.json
@@ -1,6 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
"error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4"
},
"step": {
diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py
index a2bd2e6e88c..636fa7a2b8a 100644
--- a/homeassistant/components/upcloud/__init__.py
+++ b/homeassistant/components/upcloud/__init__.py
@@ -162,11 +162,11 @@ async def _async_signal_options_update(
)
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the UpCloud config entry."""
manager = upcloud_api.CloudManager(
- config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD]
+ entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]
)
try:
@@ -182,20 +182,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
# Handle pre config entry (0.117) scan interval migration to options
migrated_scan_interval = upcloud_data.scan_interval_migrations.pop(
- config_entry.data[CONF_USERNAME], None
+ entry.data[CONF_USERNAME], None
)
if migrated_scan_interval and (
- not config_entry.options.get(CONF_SCAN_INTERVAL)
- or config_entry.options[CONF_SCAN_INTERVAL]
- == DEFAULT_SCAN_INTERVAL.total_seconds()
+ not entry.options.get(CONF_SCAN_INTERVAL)
+ or entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL.total_seconds()
):
update_interval = migrated_scan_interval
hass.config_entries.async_update_entry(
- config_entry,
+ entry,
options={CONF_SCAN_INTERVAL: update_interval.total_seconds()},
)
- elif config_entry.options.get(CONF_SCAN_INTERVAL):
- update_interval = timedelta(seconds=config_entry.options[CONF_SCAN_INTERVAL])
+ elif entry.options.get(CONF_SCAN_INTERVAL):
+ update_interval = timedelta(seconds=entry.options[CONF_SCAN_INTERVAL])
else:
update_interval = DEFAULT_SCAN_INTERVAL
@@ -203,28 +202,26 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
hass,
update_interval=update_interval,
cloud_manager=manager,
- username=config_entry.data[CONF_USERNAME],
+ username=entry.data[CONF_USERNAME],
)
# Call the UpCloud API to refresh data
await coordinator.async_config_entry_first_refresh()
# Listen to config entry updates
- config_entry.async_on_unload(
- config_entry.add_update_listener(_async_signal_options_update)
- )
- config_entry.async_on_unload(
+ entry.async_on_unload(entry.add_update_listener(_async_signal_options_update))
+ entry.async_on_unload(
async_dispatcher_connect(
hass,
- _config_entry_update_signal_name(config_entry),
+ _config_entry_update_signal_name(entry),
coordinator.async_update_config,
)
)
- upcloud_data.coordinators[config_entry.data[CONF_USERNAME]] = coordinator
+ upcloud_data.coordinators[entry.data[CONF_USERNAME]] = coordinator
# Forward entry setup
- hass.config_entries.async_setup_platforms(config_entry, CONFIG_ENTRY_DOMAINS)
+ hass.config_entries.async_setup_platforms(entry, CONFIG_ENTRY_DOMAINS)
return True
@@ -243,6 +240,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
class UpCloudServerEntity(CoordinatorEntity):
"""Entity class for UpCloud servers."""
+ _attr_device_class = DEFAULT_COMPONENT_DEVICE_CLASS
+
def __init__(
self,
coordinator: DataUpdateCoordinator[dict[str, upcloud_api.Server]],
@@ -274,23 +273,20 @@ class UpCloudServerEntity(CoordinatorEntity):
"""Return the icon of this server."""
return "mdi:server" if self.is_on else "mdi:server-off"
- @property
- def state(self) -> str | None:
- """Return state of the server."""
- try:
- return STATE_MAP.get(self._server.state, self._server.state)
- except AttributeError:
- return None
-
@property
def is_on(self) -> bool:
"""Return true if the server is on."""
- return self.state == STATE_ON
+ try:
+ return STATE_MAP.get(self._server.state, self._server.state) == STATE_ON
+ except AttributeError:
+ return False
@property
- def device_class(self) -> str:
- """Return the class of this server."""
- return DEFAULT_COMPONENT_DEVICE_CLASS
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return super().available and STATE_MAP.get(
+ self._server.state, self._server.state
+ ) in [STATE_ON, STATE_OFF]
@property
def extra_state_attributes(self) -> dict[str, Any]:
diff --git a/homeassistant/components/upcloud/translations/he.json b/homeassistant/components/upcloud/translations/he.json
new file mode 100644
index 00000000000..b0279f2d508
--- /dev/null
+++ b/homeassistant/components/upcloud/translations/he.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py
index 7edf7b99d36..5788ec1b3ef 100644
--- a/homeassistant/components/upnp/__init__.py
+++ b/homeassistant/components/upnp/__init__.py
@@ -5,6 +5,7 @@ from ipaddress import ip_address
import voluptuous as vol
from homeassistant import config_entries
+from homeassistant.components import ssdp
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
@@ -17,9 +18,6 @@ from .const import (
CONFIG_ENTRY_HOSTNAME,
CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN,
- DISCOVERY_LOCATION,
- DISCOVERY_ST,
- DISCOVERY_UDN,
DOMAIN,
DOMAIN_CONFIG,
DOMAIN_DEVICES,
@@ -49,24 +47,15 @@ async def async_construct_device(hass: HomeAssistant, udn: str, st: str) -> Devi
"""Discovery devices and construct a Device for one."""
# pylint: disable=invalid-name
_LOGGER.debug("Constructing device: %s::%s", udn, st)
+ discovery_info = ssdp.async_get_discovery_info_by_udn_st(hass, udn, st)
- discoveries = [
- discovery
- for discovery in await Device.async_discover(hass)
- if discovery[DISCOVERY_UDN] == udn and discovery[DISCOVERY_ST] == st
- ]
- if not discoveries:
+ if not discovery_info:
_LOGGER.info("Device not discovered")
return None
- # Some additional clues for remote debugging.
- if len(discoveries) > 1:
- _LOGGER.info("Multiple devices discovered: %s", discoveries)
-
- discovery = discoveries[0]
- _LOGGER.debug("Constructing from discovery: %s", discovery)
- location = discovery[DISCOVERY_LOCATION]
- return await Device.async_create_device(hass, location)
+ return await Device.async_create_device(
+ hass, discovery_info[ssdp.ATTR_SSDP_LOCATION]
+ )
async def async_setup(hass: HomeAssistant, config: ConfigType):
@@ -92,13 +81,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType):
return True
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up UPnP/IGD device from a config entry."""
- _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id)
+ _LOGGER.debug("Setting up config entry: %s", entry.unique_id)
# Discover and construct.
- udn = config_entry.data[CONFIG_ENTRY_UDN]
- st = config_entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name
+ udn = entry.data[CONFIG_ENTRY_UDN]
+ st = entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name
try:
device = await async_construct_device(hass, udn, st)
except asyncio.TimeoutError as err:
@@ -112,31 +101,31 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
hass.data[DOMAIN][DOMAIN_DEVICES][device.udn] = device
# Ensure entry has a unique_id.
- if not config_entry.unique_id:
+ if not entry.unique_id:
_LOGGER.debug(
"Setting unique_id: %s, for config_entry: %s",
device.unique_id,
- config_entry,
+ entry,
)
hass.config_entries.async_update_entry(
- entry=config_entry,
+ entry=entry,
unique_id=device.unique_id,
)
# Ensure entry has a hostname, for older entries.
if (
- CONFIG_ENTRY_HOSTNAME not in config_entry.data
- or config_entry.data[CONFIG_ENTRY_HOSTNAME] != device.hostname
+ CONFIG_ENTRY_HOSTNAME not in entry.data
+ or entry.data[CONFIG_ENTRY_HOSTNAME] != device.hostname
):
hass.config_entries.async_update_entry(
- entry=config_entry,
- data={CONFIG_ENTRY_HOSTNAME: device.hostname, **config_entry.data},
+ entry=entry,
+ data={CONFIG_ENTRY_HOSTNAME: device.hostname, **entry.data},
)
# Create device registry entry.
device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create(
- config_entry_id=config_entry.entry_id,
+ config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_UPNP, device.udn)},
identifiers={(DOMAIN, device.udn)},
name=device.name,
@@ -146,7 +135,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
# Create sensors.
_LOGGER.debug("Enabling sensors")
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
# Start device updater.
await device.async_start()
diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py
index 8e2dfc1f43f..f52ce89660d 100644
--- a/homeassistant/components/upnp/config_flow.py
+++ b/homeassistant/components/upnp/config_flow.py
@@ -29,17 +29,7 @@ from .const import (
DOMAIN_DEVICES,
LOGGER as _LOGGER,
)
-from .device import Device
-
-
-def discovery_info_to_discovery(discovery_info: Mapping) -> Mapping:
- """Convert a SSDP-discovery to 'our' discovery."""
- return {
- DISCOVERY_UDN: discovery_info[ssdp.ATTR_UPNP_UDN],
- DISCOVERY_ST: discovery_info[ssdp.ATTR_SSDP_ST],
- DISCOVERY_LOCATION: discovery_info[ssdp.ATTR_SSDP_LOCATION],
- DISCOVERY_USN: discovery_info[ssdp.ATTR_SSDP_USN],
- }
+from .device import Device, discovery_info_to_discovery
class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py
index 496293926d3..9af7cf55c24 100644
--- a/homeassistant/components/upnp/device.py
+++ b/homeassistant/components/upnp/device.py
@@ -12,6 +12,7 @@ from async_upnp_client.aiohttp import AiohttpSessionRequester
from async_upnp_client.device_updater import DeviceUpdater
from async_upnp_client.profiles.igd import IgdDevice
+from homeassistant.components import ssdp
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@@ -37,6 +38,16 @@ from .const import (
)
+def discovery_info_to_discovery(discovery_info: Mapping) -> Mapping:
+ """Convert a SSDP-discovery to 'our' discovery."""
+ return {
+ DISCOVERY_UDN: discovery_info[ssdp.ATTR_UPNP_UDN],
+ DISCOVERY_ST: discovery_info[ssdp.ATTR_SSDP_ST],
+ DISCOVERY_LOCATION: discovery_info[ssdp.ATTR_SSDP_LOCATION],
+ DISCOVERY_USN: discovery_info[ssdp.ATTR_SSDP_USN],
+ }
+
+
def _get_local_ip(hass: HomeAssistant) -> IPv4Address | None:
"""Get the configured local ip."""
if DOMAIN in hass.data and DOMAIN_CONFIG in hass.data[DOMAIN]:
@@ -59,17 +70,10 @@ class Device:
async def async_discover(cls, hass: HomeAssistant) -> list[Mapping]:
"""Discover UPnP/IGD devices."""
_LOGGER.debug("Discovering UPnP/IGD devices")
- local_ip = _get_local_ip(hass)
- discoveries = await IgdDevice.async_search(source_ip=local_ip, timeout=10)
-
- # Supplement/standardize discovery.
- for discovery in discoveries:
- discovery[DISCOVERY_UDN] = discovery["_udn"]
- discovery[DISCOVERY_ST] = discovery["st"]
- discovery[DISCOVERY_LOCATION] = discovery["location"]
- discovery[DISCOVERY_USN] = discovery["usn"]
- _LOGGER.debug("Discovered device: %s", discovery)
-
+ discoveries = []
+ for ssdp_st in IgdDevice.DEVICE_TYPES:
+ for discovery_info in ssdp.async_get_discovery_info_by_st(hass, ssdp_st):
+ discoveries.append(discovery_info_to_discovery(discovery_info))
return discoveries
@classmethod
diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json
index b130e721e35..b252f5082cb 100644
--- a/homeassistant/components/upnp/manifest.json
+++ b/homeassistant/components/upnp/manifest.json
@@ -3,7 +3,8 @@
"name": "UPnP/IGD",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/upnp",
- "requirements": ["async-upnp-client==0.18.0"],
+ "requirements": ["async-upnp-client==0.19.0"],
+ "dependencies": ["ssdp"],
"codeowners": ["@StevenLooman"],
"ssdp": [
{
diff --git a/homeassistant/components/upnp/translations/de.json b/homeassistant/components/upnp/translations/de.json
index 8a5f4fefadf..a43700ba236 100644
--- a/homeassistant/components/upnp/translations/de.json
+++ b/homeassistant/components/upnp/translations/de.json
@@ -9,7 +9,7 @@
"one": "Ein",
"other": "andere"
},
- "flow_title": "UPnP/IGD: {name}",
+ "flow_title": "{name}",
"step": {
"ssdp_confirm": {
"description": "M\u00f6chten Sie dieses UPnP/IGD-Ger\u00e4t einrichten?"
@@ -17,9 +17,19 @@
"user": {
"data": {
"scan_interval": "Aktualisierungsintervall (Sekunden, mindestens 30)",
+ "unique_id": "Ger\u00e4t",
"usn": "Ger\u00e4t"
}
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Aktualisierungsintervall (Sekunden, minimal 30)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/translations/es.json b/homeassistant/components/upnp/translations/es.json
index 2165979a75d..356376e2e07 100644
--- a/homeassistant/components/upnp/translations/es.json
+++ b/homeassistant/components/upnp/translations/es.json
@@ -17,9 +17,19 @@
"user": {
"data": {
"scan_interval": "Intervalo de actualizaci\u00f3n (segundos, m\u00ednimo 30)",
+ "unique_id": "Dispositivo",
"usn": "Dispositivo"
}
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Intervalo de actualizaci\u00f3n (segundos, m\u00ednimo 30)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/translations/he.json b/homeassistant/components/upnp/translations/he.json
index 4b922ccd2ba..706d87f0db4 100644
--- a/homeassistant/components/upnp/translations/he.json
+++ b/homeassistant/components/upnp/translations/he.json
@@ -1,8 +1,20 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea"
+ },
+ "error": {
+ "many": "",
+ "one": "\u05e8\u05d9\u05e7",
+ "other": "\u05e8\u05d9\u05e7\u05d9\u05dd",
+ "two": "\u05e8\u05d9\u05e7\u05d9\u05dd"
+ },
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
+ "unique_id": "\u05d4\u05ea\u05e7\u05df",
"usn": "\u05de\u05db\u05e9\u05d9\u05e8"
}
}
diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json
index 8b50de71f74..0bffeeaf154 100644
--- a/homeassistant/components/upnp/translations/hu.json
+++ b/homeassistant/components/upnp/translations/hu.json
@@ -8,7 +8,7 @@
"one": "hiba",
"other": ""
},
- "flow_title": "UPnP/IGD: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/upnp/translations/it.json b/homeassistant/components/upnp/translations/it.json
index 67a3a385dbc..0e00d002422 100644
--- a/homeassistant/components/upnp/translations/it.json
+++ b/homeassistant/components/upnp/translations/it.json
@@ -11,6 +11,10 @@
},
"flow_title": "{name}",
"step": {
+ "init": {
+ "one": "Pi\u00f9",
+ "other": "Altri"
+ },
"ssdp_confirm": {
"description": "Vuoi configurare questo dispositivo UPnP/IGD?"
},
diff --git a/homeassistant/components/upnp/translations/pl.json b/homeassistant/components/upnp/translations/pl.json
index 3f66ec7c6f9..30213436d27 100644
--- a/homeassistant/components/upnp/translations/pl.json
+++ b/homeassistant/components/upnp/translations/pl.json
@@ -25,9 +25,19 @@
"user": {
"data": {
"scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (sekundy, minimum 30)",
+ "unique_id": "Urz\u0105dzenie",
"usn": "Urz\u0105dzenie"
}
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (sekundy, minimum 30)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py
index 7e79c2fbb5e..98c673b8878 100644
--- a/homeassistant/components/uptime/sensor.py
+++ b/homeassistant/components/uptime/sensor.py
@@ -1,14 +1,18 @@
"""Platform to retrieve uptime for Home Assistant."""
+from __future__ import annotations
import voluptuous as vol
-from homeassistant.components.sensor import (
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import (
+ CONF_NAME,
+ CONF_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_TIMESTAMP,
- PLATFORM_SCHEMA,
- SensorEntity,
)
-from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
+from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.dt as dt_util
DEFAULT_NAME = "Uptime"
@@ -26,9 +30,14 @@ PLATFORM_SCHEMA = vol.All(
)
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
"""Set up the uptime sensor platform."""
- name = config.get(CONF_NAME)
+ name = config[CONF_NAME]
async_add_entities([UptimeSensor(name)], True)
@@ -36,23 +45,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class UptimeSensor(SensorEntity):
"""Representation of an uptime sensor."""
- def __init__(self, name):
+ def __init__(self, name: str) -> None:
"""Initialize the uptime sensor."""
self._name = name
self._state = dt_util.now().isoformat()
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
- def device_class(self):
+ def device_class(self) -> str:
"""Return device class."""
return DEVICE_CLASS_TIMESTAMP
@property
- def state(self):
+ def state(self) -> str:
"""Return the state of the sensor."""
return self._state
diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py
index d8803931f38..36cc632d932 100644
--- a/homeassistant/components/vacuum/__init__.py
+++ b/homeassistant/components/vacuum/__init__.py
@@ -6,6 +6,7 @@ from typing import final
import voluptuous as vol
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API
ATTR_BATTERY_LEVEL,
ATTR_COMMAND,
@@ -16,6 +17,7 @@ from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API
STATE_ON,
STATE_PAUSED,
)
+from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
@@ -122,14 +124,16 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class _BaseVacuum(Entity):
diff --git a/homeassistant/components/vacuum/device_action.py b/homeassistant/components/vacuum/device_action.py
index 2308882469e..a4df68c3b93 100644
--- a/homeassistant/components/vacuum/device_action.py
+++ b/homeassistant/components/vacuum/device_action.py
@@ -36,22 +36,14 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
if entry.domain != DOMAIN:
continue
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "clean",
- }
- )
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "dock",
- }
- )
+ base_action = {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
+ actions.append({**base_action, CONF_TYPE: "clean"})
+ actions.append({**base_action, CONF_TYPE: "dock"})
return actions
diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py
index 4803ebdb988..a66df1323f7 100644
--- a/homeassistant/components/vacuum/device_condition.py
+++ b/homeassistant/components/vacuum/device_condition.py
@@ -40,24 +40,14 @@ async def async_get_conditions(
if entry.domain != DOMAIN:
continue
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_cleaning",
- }
- )
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_docked",
- }
- )
+ base_condition = {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
+ conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES]
return conditions
diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py
index d5c596b209a..4c1d6e93820 100644
--- a/homeassistant/components/vacuum/device_trigger.py
+++ b/homeassistant/components/vacuum/device_trigger.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import state as state_trigger
from homeassistant.const import (
CONF_DEVICE_ID,
@@ -22,7 +22,7 @@ from . import DOMAIN, STATE_CLEANING, STATE_DOCKED
TRIGGER_TYPES = {"cleaning", "docked"}
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
diff --git a/homeassistant/components/vacuum/translations/he.json b/homeassistant/components/vacuum/translations/he.json
index dc6b5da01cb..82e0d073406 100644
--- a/homeassistant/components/vacuum/translations/he.json
+++ b/homeassistant/components/vacuum/translations/he.json
@@ -1,14 +1,28 @@
{
+ "device_automation": {
+ "action_type": {
+ "clean": "\u05d0\u05e4\u05e9\u05e8 \u05dc-{entity_name} \u05dc\u05e0\u05e7\u05d5\u05ea",
+ "dock": "\u05d0\u05e4\u05e9\u05e8 \u05dc-{entity_name} \u05dc\u05d7\u05d6\u05d5\u05e8 \u05dc\u05ea\u05d7\u05e0\u05ea \u05d8\u05e2\u05d9\u05e0\u05d4"
+ },
+ "condition_type": {
+ "is_cleaning": "{entity_name} \u05de\u05e0\u05e7\u05d4",
+ "is_docked": "{entity_name} \u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4"
+ },
+ "trigger_type": {
+ "cleaning": "{entity_name} \u05de\u05ea\u05d7\u05d9\u05dc \u05dc\u05e0\u05e7\u05d5\u05ea",
+ "docked": "{entity_name} \u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4"
+ }
+ },
"state": {
"_": {
"cleaning": "\u05de\u05e0\u05e7\u05d4",
- "docked": "\u05d1\u05e2\u05d2\u05d9\u05e0\u05d4",
+ "docked": "\u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05d8\u05e2\u05d9\u05e0\u05d4",
"error": "\u05e9\u05d2\u05d9\u05d0\u05d4",
"idle": "\u05de\u05de\u05ea\u05d9\u05df",
- "off": "\u05de\u05db\u05d5\u05d1\u05d4",
+ "off": "\u05db\u05d1\u05d5\u05d9",
"on": "\u05de\u05d5\u05e4\u05e2\u05dc",
"paused": "\u05de\u05d5\u05e9\u05d4\u05d4",
- "returning": "\u05d7\u05d6\u05d5\u05e8 \u05dc\u05e2\u05d2\u05d9\u05e0\u05d4"
+ "returning": "\u05d7\u05d5\u05d6\u05e8 \u05dc\u05ea\u05d7\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4"
}
},
"title": "\u05e9\u05d5\u05d0\u05d1 \u05d0\u05d1\u05e7"
diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py
index 47f51d8b26e..b798023c465 100644
--- a/homeassistant/components/velbus/__init__.py
+++ b/homeassistant/components/velbus/__init__.py
@@ -43,7 +43,7 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Establish connection with velbus."""
hass.data.setdefault(DOMAIN, {})
diff --git a/homeassistant/components/velbus/translations/he.json b/homeassistant/components/velbus/translations/he.json
new file mode 100644
index 00000000000..c1b4500289b
--- /dev/null
+++ b/homeassistant/components/velbus/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py
index 096c6a8aa15..feac63f694b 100644
--- a/homeassistant/components/vera/__init__.py
+++ b/homeassistant/components/vera/__init__.py
@@ -83,30 +83,30 @@ async def async_setup(hass: HomeAssistant, base_config: dict) -> bool:
return True
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Do setup of vera."""
# Use options entered during initial config flow or provided from configuration.yml
- if config_entry.data.get(CONF_LIGHTS) or config_entry.data.get(CONF_EXCLUDE):
+ if entry.data.get(CONF_LIGHTS) or entry.data.get(CONF_EXCLUDE):
hass.config_entries.async_update_entry(
- entry=config_entry,
- data=config_entry.data,
+ entry=entry,
+ data=entry.data,
options=new_options(
- config_entry.data.get(CONF_LIGHTS, []),
- config_entry.data.get(CONF_EXCLUDE, []),
+ entry.data.get(CONF_LIGHTS, []),
+ entry.data.get(CONF_EXCLUDE, []),
),
)
- saved_light_ids = config_entry.options.get(CONF_LIGHTS, [])
- saved_exclude_ids = config_entry.options.get(CONF_EXCLUDE, [])
+ saved_light_ids = entry.options.get(CONF_LIGHTS, [])
+ saved_exclude_ids = entry.options.get(CONF_EXCLUDE, [])
- base_url = config_entry.data[CONF_CONTROLLER]
+ base_url = entry.data[CONF_CONTROLLER]
light_ids = fix_device_id_list(saved_light_ids)
exclude_ids = fix_device_id_list(saved_exclude_ids)
# If the ids were corrected. Update the config entry.
if light_ids != saved_light_ids or exclude_ids != saved_exclude_ids:
hass.config_entries.async_update_entry(
- entry=config_entry, options=new_options(light_ids, exclude_ids)
+ entry=entry, options=new_options(light_ids, exclude_ids)
)
# Initialize the Vera controller.
@@ -139,15 +139,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
controller=controller,
devices=vera_devices,
scenes=vera_scenes,
- config_entry=config_entry,
+ config_entry=entry,
)
- set_controller_data(hass, config_entry, controller_data)
+ set_controller_data(hass, entry, controller_data)
# Forward the config data to the necessary platforms.
for platform in get_configured_platforms(controller_data):
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
def stop_subscription(event):
@@ -155,13 +155,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
controller.stop()
await hass.async_add_executor_job(controller.start)
- config_entry.async_on_unload(
+ entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
)
- config_entry.async_on_unload(
- config_entry.add_update_listener(_async_update_listener)
- )
+ entry.async_on_unload(entry.add_update_listener(_async_update_listener))
return True
diff --git a/homeassistant/components/vera/translations/de.json b/homeassistant/components/vera/translations/de.json
index f0a680e7c2d..7fce8f6bdfe 100644
--- a/homeassistant/components/vera/translations/de.json
+++ b/homeassistant/components/vera/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "Konnte keine Verbindung zum Controller mit url {base_url} herstellen"
+ "cannot_connect": "Konnte keine Verbindung zum Controller mit URL {base_url} herstellen"
},
"step": {
"user": {
@@ -10,7 +10,7 @@
"lights": "Vera Switch-Ger\u00e4te-IDs, die im Home Assistant als Lichter behandelt werden sollen.",
"vera_controller_url": "Controller-URL"
},
- "description": "Stellen Sie unten eine Vera-Controller-Url zur Verf\u00fcgung. Sie sollte wie folgt aussehen: http://192.168.1.161:3480.",
+ "description": "Stellen Sie unten eine Vera-Controller-URL zur Verf\u00fcgung. Sie sollte wie folgt aussehen: http://192.168.1.161:3480.",
"title": "Richten Sie den Vera-Controller ein"
}
}
diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py
index 4def470ac5e..176ca9444c1 100644
--- a/homeassistant/components/verisure/alarm_control_panel.py
+++ b/homeassistant/components/verisure/alarm_control_panel.py
@@ -35,8 +35,9 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity):
coordinator: VerisureDataUpdateCoordinator
+ _attr_code_format = FORMAT_NUMBER
_attr_name = "Verisure Alarm"
- _changed_by: str | None = None
+ _attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY
@property
def device_info(self) -> DeviceInfo:
@@ -48,26 +49,11 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity):
"identifiers": {(DOMAIN, self.coordinator.entry.data[CONF_GIID])},
}
- @property
- def supported_features(self) -> int:
- """Return the list of supported features."""
- return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY
-
@property
def unique_id(self) -> str:
"""Return the unique ID for this entity."""
return self.coordinator.entry.data[CONF_GIID]
- @property
- def code_format(self) -> str:
- """Return one or more digits/characters."""
- return FORMAT_NUMBER
-
- @property
- def changed_by(self) -> str | None:
- """Return the last change triggered by."""
- return self._changed_by
-
async def _async_set_arm_state(self, state: str, code: str | None = None) -> None:
"""Send set arm state command."""
arm_state = await self.hass.async_add_executor_job(
@@ -102,7 +88,7 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity):
self._attr_state = ALARM_STATE_TO_HA.get(
self.coordinator.data["alarm"]["statusType"]
)
- self._changed_by = self.coordinator.data["alarm"].get("name")
+ self._attr_changed_by = self.coordinator.data["alarm"].get("name")
super()._handle_coordinator_update()
async def async_added_to_hass(self) -> None:
diff --git a/homeassistant/components/verisure/translations/he.json b/homeassistant/components/verisure/translations/he.json
new file mode 100644
index 00000000000..ba0af14615f
--- /dev/null
+++ b/homeassistant/components/verisure/translations/he.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "email": "\u05d3\u05d5\u05d0\"\u05dc",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ },
+ "user": {
+ "data": {
+ "email": "\u05d3\u05d5\u05d0\"\u05dc",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/translations/he.json b/homeassistant/components/vesync/translations/he.json
index 6f4191da70d..a5aa8e05f22 100644
--- a/homeassistant/components/vesync/translations/he.json
+++ b/homeassistant/components/vesync/translations/he.json
@@ -1,10 +1,18 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea."
+ },
+ "error": {
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
"step": {
"user": {
"data": {
- "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
- }
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05d3\u05d5\u05d0\"\u05dc"
+ },
+ "title": "\u05d4\u05d6\u05df \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05e1\u05d9\u05e1\u05de\u05d4"
}
}
}
diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py
index 59387fa81c8..9a65ba3c400 100644
--- a/homeassistant/components/vilfo/__init__.py
+++ b/homeassistant/components/vilfo/__init__.py
@@ -20,7 +20,7 @@ DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Vilfo Router from a config entry."""
host = entry.data[CONF_HOST]
access_token = entry.data[CONF_ACCESS_TOKEN]
diff --git a/homeassistant/components/vilfo/translations/he.json b/homeassistant/components/vilfo/translations/he.json
new file mode 100644
index 00000000000..642f482d14e
--- /dev/null
+++ b/homeassistant/components/vilfo/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "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",
+ "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": {
+ "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4",
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ },
+ "description": "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e0\u05ea\u05d1 \u05e9\u05dc Vilfo. \u05d0\u05ea\u05d4 \u05e6\u05e8\u05d9\u05da \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7/IP \u05e9\u05dc \u05e0\u05ea\u05d1 Vilfo \u05e9\u05dc\u05da \u05d5\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05ea API. \u05dc\u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05e2\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d6\u05d4 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05e7\u05d1\u05dc \u05e4\u05e8\u05d8\u05d9\u05dd \u05d0\u05dc\u05d4, \u05d1\u05e7\u05e8 \u05d1\u05db\u05ea\u05d5\u05d1\u05ea: https://www.home-assistant.io/integrations/vilfo"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py
index 7c1ed7e8fa7..aec6f38a1b1 100644
--- a/homeassistant/components/vizio/__init__.py
+++ b/homeassistant/components/vizio/__init__.py
@@ -56,19 +56,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Load the saved entities."""
hass.data.setdefault(DOMAIN, {})
if (
CONF_APPS not in hass.data[DOMAIN]
- and config_entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV
+ and entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV
):
coordinator = VizioAppsDataUpdateCoordinator(hass)
await coordinator.async_refresh()
hass.data[DOMAIN][CONF_APPS] = coordinator
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
diff --git a/homeassistant/components/vizio/translations/he.json b/homeassistant/components/vizio/translations/he.json
new file mode 100644
index 00000000000..977db5ff9f3
--- /dev/null
+++ b/homeassistant/components/vizio/translations/he.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "step": {
+ "pair_tv": {
+ "data": {
+ "pin": "\u05e7\u05d5\u05d3 PIN"
+ }
+ },
+ "user": {
+ "data": {
+ "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4",
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "name": "\u05e9\u05dd"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/volumio/__init__.py b/homeassistant/components/volumio/__init__.py
index f9b9432d755..6e0db0f73eb 100644
--- a/homeassistant/components/volumio/__init__.py
+++ b/homeassistant/components/volumio/__init__.py
@@ -13,7 +13,7 @@ from .const import DATA_INFO, DATA_VOLUMIO, DOMAIN
PLATFORMS = ["media_player"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Volumio from a config entry."""
volumio = Volumio(
diff --git a/homeassistant/components/volumio/translations/he.json b/homeassistant/components/volumio/translations/he.json
new file mode 100644
index 00000000000..58521f503e2
--- /dev/null
+++ b/homeassistant/components/volumio/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vultr/binary_sensor.py b/homeassistant/components/vultr/binary_sensor.py
index c62d5136aa6..f5f03c62872 100644
--- a/homeassistant/components/vultr/binary_sensor.py
+++ b/homeassistant/components/vultr/binary_sensor.py
@@ -54,6 +54,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class VultrBinarySensor(BinarySensorEntity):
"""Representation of a Vultr subscription sensor."""
+ _attr_device_class = DEFAULT_DEVICE_CLASS
+
def __init__(self, vultr, subscription, name):
"""Initialize a new Vultr binary sensor."""
self._vultr = vultr
@@ -80,11 +82,6 @@ class VultrBinarySensor(BinarySensorEntity):
"""Return true if the binary sensor is on."""
return self.data["power_status"] == "running"
- @property
- def device_class(self):
- """Return the class of this sensor."""
- return DEFAULT_DEVICE_CLASS
-
@property
def extra_state_attributes(self):
"""Return the state attributes of the Vultr subscription."""
diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py
index 97b2ea12f35..aeacee9b943 100644
--- a/homeassistant/components/wallbox/__init__.py
+++ b/homeassistant/components/wallbox/__init__.py
@@ -1,5 +1,4 @@
"""The Wallbox integration."""
-import asyncio
from datetime import timedelta
import logging
@@ -89,7 +88,7 @@ class WallboxHub:
return self._coordinator
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Wallbox from a config entry."""
wallbox = WallboxHub(
entry.data[CONF_STATION],
@@ -115,33 +114,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
- unload_ok = all(
- await asyncio.gather(
- *[
- hass.config_entries.async_forward_entry_unload(entry, platform)
- for platform in PLATFORMS
- ]
- )
- )
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN]["connections"].pop(entry.entry_id)
return unload_ok
-class CannotConnect(exceptions.HomeAssistantError):
- """Error to indicate we cannot connect."""
-
- def __init__(self, msg=""):
- """Create a log record."""
- super().__init__()
- _LOGGER.error("Cannot connect to Wallbox API. %s", msg)
-
-
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""
-
- def __init__(self, msg=""):
- """Create a log record."""
- super().__init__()
- _LOGGER.error("Cannot authenticate with Wallbox API. %s", msg)
diff --git a/homeassistant/components/wallbox/config_flow.py b/homeassistant/components/wallbox/config_flow.py
index 69b01d96c40..f9fdef3c5af 100644
--- a/homeassistant/components/wallbox/config_flow.py
+++ b/homeassistant/components/wallbox/config_flow.py
@@ -4,7 +4,7 @@ import voluptuous as vol
from homeassistant import config_entries, core
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from . import CannotConnect, InvalidAuth, WallboxHub
+from . import InvalidAuth, WallboxHub
from .const import CONF_STATION, DOMAIN
COMPONENT_DOMAIN = DOMAIN
@@ -46,7 +46,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN):
try:
info = await validate_input(self.hass, user_input)
- except CannotConnect:
+ except ConnectionError:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
diff --git a/homeassistant/components/wallbox/translations/ca.json b/homeassistant/components/wallbox/translations/ca.json
new file mode 100644
index 00000000000..55240065548
--- /dev/null
+++ b/homeassistant/components/wallbox/translations/ca.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrasenya",
+ "station": "N\u00famero de s\u00e8rie de l'estaci\u00f3",
+ "username": "Nom d'usuari"
+ }
+ }
+ }
+ },
+ "title": "Wallbox"
+}
\ No newline at end of file
diff --git a/homeassistant/components/wallbox/translations/de.json b/homeassistant/components/wallbox/translations/de.json
new file mode 100644
index 00000000000..89362597b85
--- /dev/null
+++ b/homeassistant/components/wallbox/translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "station": "Seriennummer",
+ "username": "Benutzername"
+ }
+ }
+ }
+ },
+ "title": "Wallbox"
+}
\ No newline at end of file
diff --git a/homeassistant/components/wallbox/translations/es.json b/homeassistant/components/wallbox/translations/es.json
new file mode 100644
index 00000000000..71e7a748955
--- /dev/null
+++ b/homeassistant/components/wallbox/translations/es.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "station": "N\u00famero de serie de la estaci\u00f3n"
+ }
+ }
+ }
+ },
+ "title": "Wallbox"
+}
\ No newline at end of file
diff --git a/homeassistant/components/wallbox/translations/he.json b/homeassistant/components/wallbox/translations/he.json
new file mode 100644
index 00000000000..ca1c7a93c5c
--- /dev/null
+++ b/homeassistant/components/wallbox/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "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",
+ "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": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ },
+ "title": "\u05ea\u05d9\u05d1\u05ea \u05e7\u05d9\u05e8"
+}
\ No newline at end of file
diff --git a/homeassistant/components/wallbox/translations/hu.json b/homeassistant/components/wallbox/translations/hu.json
new file mode 100644
index 00000000000..fd8db27da5e
--- /dev/null
+++ b/homeassistant/components/wallbox/translations/hu.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wallbox/translations/it.json b/homeassistant/components/wallbox/translations/it.json
new file mode 100644
index 00000000000..5b8828860e7
--- /dev/null
+++ b/homeassistant/components/wallbox/translations/it.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "station": "Numero di serie della stazione",
+ "username": "Nome utente"
+ }
+ }
+ }
+ },
+ "title": "Wallbox"
+}
\ No newline at end of file
diff --git a/homeassistant/components/wallbox/translations/pl.json b/homeassistant/components/wallbox/translations/pl.json
new file mode 100644
index 00000000000..2728f1cae31
--- /dev/null
+++ b/homeassistant/components/wallbox/translations/pl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "station": "Numer seryjny stacji",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ },
+ "title": "Wallbox"
+}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py
index 5ae22c77b5e..fcee7a446e0 100644
--- a/homeassistant/components/water_heater/__init__.py
+++ b/homeassistant/components/water_heater/__init__.py
@@ -1,4 +1,6 @@
"""Support for water heater devices."""
+from __future__ import annotations
+
from datetime import timedelta
import functools as ft
import logging
@@ -6,6 +8,7 @@ from typing import final
import voluptuous as vol
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_TEMPERATURE,
@@ -18,6 +21,7 @@ from homeassistant.const import (
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
+from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
@@ -119,27 +123,46 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class WaterHeaterEntity(Entity):
"""Base class for water heater entities."""
+ _attr_current_operation: str | None = None
+ _attr_current_temperature: float | None = None
+ _attr_is_away_mode_on: bool | None = None
+ _attr_max_temp: float
+ _attr_min_temp: float
+ _attr_operation_list: list[str] | None = None
+ _attr_precision: float
+ _attr_state: None = None
+ _attr_supported_features: int
+ _attr_target_temperature_high: float | None = None
+ _attr_target_temperature_low: float | None = None
+ _attr_target_temperature: float | None = None
+ _attr_temperature_unit: str
+
+ @final
@property
- def state(self):
+ def state(self) -> str | None:
"""Return the current state."""
return self.current_operation
@property
- def precision(self):
+ def precision(self) -> float:
"""Return the precision of the system."""
+ if hasattr(self, "_attr_precision"):
+ return self._attr_precision
if self.hass.config.units.temperature_unit == TEMP_CELSIUS:
return PRECISION_TENTHS
return PRECISION_WHOLE
@@ -206,44 +229,44 @@ class WaterHeaterEntity(Entity):
return data
@property
- def temperature_unit(self):
+ def temperature_unit(self) -> str:
"""Return the unit of measurement used by the platform."""
- raise NotImplementedError
+ return self._attr_temperature_unit
@property
- def current_operation(self):
+ def current_operation(self) -> str | None:
"""Return current operation ie. eco, electric, performance, ..."""
- return None
+ return self._attr_current_operation
@property
- def operation_list(self):
+ def operation_list(self) -> list[str] | None:
"""Return the list of available operation modes."""
- return None
+ return self._attr_operation_list
@property
- def current_temperature(self):
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
- return None
+ return self._attr_current_temperature
@property
- def target_temperature(self):
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
- return None
+ return self._attr_target_temperature
@property
- def target_temperature_high(self):
+ def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
- return None
+ return self._attr_target_temperature_high
@property
- def target_temperature_low(self):
+ def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
- return None
+ return self._attr_target_temperature_low
@property
- def is_away_mode_on(self):
+ def is_away_mode_on(self) -> bool | None:
"""Return true if away mode is on."""
- return None
+ return self._attr_is_away_mode_on
def set_temperature(self, **kwargs):
"""Set new target temperature."""
@@ -279,14 +302,11 @@ class WaterHeaterEntity(Entity):
"""Turn away mode off."""
await self.hass.async_add_executor_job(self.turn_away_mode_off)
- @property
- def supported_features(self):
- """Return the list of supported features."""
- raise NotImplementedError()
-
@property
def min_temp(self):
"""Return the minimum temperature."""
+ if hasattr(self, "_attr_min_temp"):
+ return self._attr_min_temp
return convert_temperature(
DEFAULT_MIN_TEMP, TEMP_FAHRENHEIT, self.temperature_unit
)
@@ -294,6 +314,8 @@ class WaterHeaterEntity(Entity):
@property
def max_temp(self):
"""Return the maximum temperature."""
+ if hasattr(self, "_attr_max_temp"):
+ return self._attr_max_temp
return convert_temperature(
DEFAULT_MAX_TEMP, TEMP_FAHRENHEIT, self.temperature_unit
)
diff --git a/homeassistant/components/water_heater/device_action.py b/homeassistant/components/water_heater/device_action.py
index e1c84be8753..3662dee9a5e 100644
--- a/homeassistant/components/water_heater/device_action.py
+++ b/homeassistant/components/water_heater/device_action.py
@@ -37,22 +37,14 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
if entry.domain != DOMAIN:
continue
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "turn_on",
- }
- )
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "turn_off",
- }
- )
+ base_action = {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
+ actions.append({**base_action, CONF_TYPE: "turn_on"})
+ actions.append({**base_action, CONF_TYPE: "turn_off"})
return actions
diff --git a/homeassistant/components/water_heater/translations/he.json b/homeassistant/components/water_heater/translations/he.json
new file mode 100644
index 00000000000..7978941f3bd
--- /dev/null
+++ b/homeassistant/components/water_heater/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "turn_off": "\u05db\u05d1\u05d4 \u05d0\u05ea {entity_name}",
+ "turn_on": "\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea {entity_name}"
+ }
+ },
+ "state": {
+ "_": {
+ "eco": "\u05d7\u05e1\u05db\u05d5\u05e0\u05d9",
+ "electric": "\u05d7\u05e9\u05de\u05dc\u05d9",
+ "gas": "\u05d2\u05d6",
+ "heat_pump": "\u05de\u05e9\u05d0\u05d1\u05ea \u05d7\u05d5\u05dd",
+ "high_demand": "\u05d1\u05d9\u05e7\u05d5\u05e9 \u05d2\u05d1\u05d5\u05d4",
+ "off": "\u05db\u05d1\u05d5\u05d9",
+ "performance": "\u05d1\u05d9\u05e6\u05d5\u05e2\u05d9\u05dd"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py
index 57382689f61..1b9db0e947a 100644
--- a/homeassistant/components/waze_travel_time/__init__.py
+++ b/homeassistant/components/waze_travel_time/__init__.py
@@ -12,18 +12,16 @@ PLATFORMS = ["sensor"]
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Load the saved entities."""
- if config_entry.unique_id is not None:
- hass.config_entries.async_update_entry(config_entry, unique_id=None)
+ if entry.unique_id is not None:
+ hass.config_entries.async_update_entry(entry, unique_id=None)
ent_reg = async_get(hass)
- for entity in async_entries_for_config_entry(ent_reg, config_entry.entry_id):
- ent_reg.async_update_entity(
- entity.entity_id, new_unique_id=config_entry.entry_id
- )
+ for entity in async_entries_for_config_entry(ent_reg, entry.entry_id):
+ ent_reg.async_update_entity(entity.entity_id, new_unique_id=entry.entry_id)
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
diff --git a/homeassistant/components/waze_travel_time/translations/de.json b/homeassistant/components/waze_travel_time/translations/de.json
index ac2a911d233..2af713eb5d1 100644
--- a/homeassistant/components/waze_travel_time/translations/de.json
+++ b/homeassistant/components/waze_travel_time/translations/de.json
@@ -13,8 +13,27 @@
"name": "Name",
"origin": "Startort",
"region": "Region"
- }
+ },
+ "description": "Geben Sie f\u00fcr Ursprung und Ziel die Adresse oder die GPS-Koordinaten des Standorts ein (GPS-Koordinaten m\u00fcssen durch ein Komma getrennt werden). Sie k\u00f6nnen auch eine Entity-ID eingeben, die diese Informationen in ihrem Zustand bereitstellt, eine Entity-ID mit den Attributen Breitengrad und L\u00e4ngengrad oder einen Zonen-Namen."
}
}
- }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid_ferries": "F\u00e4hren meiden?",
+ "avoid_subscription_roads": "Stra\u00dfen vermeiden, die eine Vignette/ ein Abonnement ben\u00f6tigen?",
+ "avoid_toll_roads": "Mautstra\u00dfen meiden?",
+ "excl_filter": "Substring NICHT in Beschreibung der ausgew\u00e4hlten Route",
+ "incl_filter": "Substring in Beschreibung der ausgew\u00e4hlten Route",
+ "realtime": "Reisezeit in Echtzeit?",
+ "units": "Einheiten",
+ "vehicle_type": "Fahrzeugtyp"
+ },
+ "description": "Mit den \"Substring\"-Eintr\u00e4gen k\u00f6nnen Sie die Integration zwingen, eine bestimmte Route zu verwenden oder eine bestimmte Route bei der Zeitreiseberechnung zu vermeiden."
+ }
+ }
+ },
+ "title": "Waze Reisezeit"
}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/he.json b/homeassistant/components/waze_travel_time/translations/he.json
new file mode 100644
index 00000000000..ab46aac243d
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/he.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \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": {
+ "destination": "\u05d9\u05e2\u05d3",
+ "name": "\u05e9\u05dd",
+ "origin": "\u05de\u05e7\u05d5\u05e8",
+ "region": "\u05d0\u05d9\u05d6\u05d5\u05e8"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "realtime": "\u05d6\u05de\u05df \u05e0\u05e1\u05d9\u05e2\u05d4 \u05d1\u05d6\u05de\u05df \u05d0\u05de\u05ea?",
+ "units": "\u05d9\u05d7\u05d9\u05d3\u05d5\u05ea"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py
index da66c354d5a..b737129e69e 100644
--- a/homeassistant/components/weather/__init__.py
+++ b/homeassistant/components/weather/__init__.py
@@ -1,9 +1,13 @@
"""Weather component that handles meteorological data for your location."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import final
+from typing import Final, TypedDict, final
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
@@ -33,15 +37,15 @@ ATTR_CONDITION_SUNNY = "sunny"
ATTR_CONDITION_WINDY = "windy"
ATTR_CONDITION_WINDY_VARIANT = "windy-variant"
ATTR_FORECAST = "forecast"
-ATTR_FORECAST_CONDITION = "condition"
-ATTR_FORECAST_PRECIPITATION = "precipitation"
-ATTR_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability"
-ATTR_FORECAST_PRESSURE = "pressure"
-ATTR_FORECAST_TEMP = "temperature"
-ATTR_FORECAST_TEMP_LOW = "templow"
-ATTR_FORECAST_TIME = "datetime"
-ATTR_FORECAST_WIND_BEARING = "wind_bearing"
-ATTR_FORECAST_WIND_SPEED = "wind_speed"
+ATTR_FORECAST_CONDITION: Final = "condition"
+ATTR_FORECAST_PRECIPITATION: Final = "precipitation"
+ATTR_FORECAST_PRECIPITATION_PROBABILITY: Final = "precipitation_probability"
+ATTR_FORECAST_PRESSURE: Final = "pressure"
+ATTR_FORECAST_TEMP: Final = "temperature"
+ATTR_FORECAST_TEMP_LOW: Final = "templow"
+ATTR_FORECAST_TIME: Final = "datetime"
+ATTR_FORECAST_WIND_BEARING: Final = "wind_bearing"
+ATTR_FORECAST_WIND_SPEED: Final = "wind_speed"
ATTR_WEATHER_ATTRIBUTION = "attribution"
ATTR_WEATHER_HUMIDITY = "humidity"
ATTR_WEATHER_OZONE = "ozone"
@@ -58,6 +62,20 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
SCAN_INTERVAL = timedelta(seconds=30)
+class Forecast(TypedDict, total=False):
+ """Typed weather forecast dict."""
+
+ condition: str | None
+ datetime: str
+ precipitation_probability: int | None
+ precipitation: float | None
+ pressure: float | None
+ temperature: float | None
+ templow: float | None
+ wind_bearing: float | str | None
+ wind_speed: float | None
+
+
async def async_setup(hass, config):
"""Set up the weather component."""
component = hass.data[DOMAIN] = EntityComponent(
@@ -67,72 +85,90 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
- return await hass.data[DOMAIN].async_setup_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_setup_entry(entry)
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.data[DOMAIN].async_unload_entry(entry)
+ component: EntityComponent = hass.data[DOMAIN]
+ return await component.async_unload_entry(entry)
class WeatherEntity(Entity):
"""ABC for weather data."""
+ _attr_attribution: str | None = None
+ _attr_condition: str | None
+ _attr_forecast: list[Forecast] | None = None
+ _attr_humidity: float | None = None
+ _attr_ozone: float | None = None
+ _attr_precision: float
+ _attr_pressure: float | None = None
+ _attr_state: None = None
+ _attr_temperature_unit: str
+ _attr_temperature: float | None
+ _attr_visibility: float | None = None
+ _attr_wind_bearing: float | str | None = None
+ _attr_wind_speed: float | None = None
+
@property
- def temperature(self):
+ def temperature(self) -> float | None:
"""Return the platform temperature."""
- raise NotImplementedError()
+ return self._attr_temperature
@property
- def temperature_unit(self):
+ def temperature_unit(self) -> str:
"""Return the unit of measurement."""
- raise NotImplementedError()
+ return self._attr_temperature_unit
@property
- def pressure(self):
+ def pressure(self) -> float | None:
"""Return the pressure."""
- return None
+ return self._attr_pressure
@property
- def humidity(self):
+ def humidity(self) -> float | None:
"""Return the humidity."""
- raise NotImplementedError()
+ return self._attr_humidity
@property
- def wind_speed(self):
+ def wind_speed(self) -> float | None:
"""Return the wind speed."""
- return None
+ return self._attr_wind_speed
@property
- def wind_bearing(self):
+ def wind_bearing(self) -> float | str | None:
"""Return the wind bearing."""
- return None
+ return self._attr_wind_bearing
@property
- def ozone(self):
+ def ozone(self) -> float | None:
"""Return the ozone level."""
- return None
+ return self._attr_ozone
@property
- def attribution(self):
+ def attribution(self) -> str | None:
"""Return the attribution."""
- return None
+ return self._attr_attribution
@property
- def visibility(self):
+ def visibility(self) -> float | None:
"""Return the visibility."""
- return None
+ return self._attr_visibility
@property
- def forecast(self):
+ def forecast(self) -> list[Forecast] | None:
"""Return the forecast."""
- return None
+ return self._attr_forecast
@property
- def precision(self):
+ def precision(self) -> float:
"""Return the precision of the temperature value."""
+ if hasattr(self, "_attr_precision"):
+ return self._attr_precision
return (
PRECISION_TENTHS
if self.temperature_unit == TEMP_CELSIUS
@@ -201,11 +237,12 @@ class WeatherEntity(Entity):
return data
@property
- def state(self):
+ @final
+ def state(self) -> str | None:
"""Return the current state."""
return self.condition
@property
- def condition(self):
+ def condition(self) -> str | None:
"""Return the current condition."""
- raise NotImplementedError()
+ return self._attr_condition
diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py
index a82dd0251c9..6bb8a61eeec 100644
--- a/homeassistant/components/webhook/trigger.py
+++ b/homeassistant/components/webhook/trigger.py
@@ -12,12 +12,15 @@ import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ("webhook",)
-TRIGGER_SCHEMA = vol.Schema(
- {vol.Required(CONF_PLATFORM): "webhook", vol.Required(CONF_WEBHOOK_ID): cv.string}
+TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_PLATFORM): "webhook",
+ vol.Required(CONF_WEBHOOK_ID): cv.string,
+ }
)
-async def _handle_webhook(job, trigger_id, hass, webhook_id, request):
+async def _handle_webhook(job, trigger_data, hass, webhook_id, request):
"""Handle incoming webhook."""
result = {"platform": "webhook", "webhook_id": webhook_id}
@@ -28,20 +31,20 @@ async def _handle_webhook(job, trigger_id, hass, webhook_id, request):
result["query"] = request.query
result["description"] = "webhook"
- result["id"] = trigger_id
+ result.update(**trigger_data)
hass.async_run_hass_job(job, {"trigger": result})
async def async_attach_trigger(hass, config, action, automation_info):
"""Trigger based on incoming webhooks."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
webhook_id = config.get(CONF_WEBHOOK_ID)
job = HassJob(action)
hass.components.webhook.async_register(
automation_info["domain"],
automation_info["name"],
webhook_id,
- partial(_handle_webhook, job, trigger_id),
+ partial(_handle_webhook, job, trigger_data),
)
@callback
diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py
index 681c2acfe01..af7f59bd266 100644
--- a/homeassistant/components/webostv/__init__.py
+++ b/homeassistant/components/webostv/__init__.py
@@ -198,7 +198,6 @@ async def async_request_configuration(hass, config, conf, client):
except (
OSError,
ConnectionClosed,
- ConnectionRefusedError,
asyncio.TimeoutError,
asyncio.CancelledError,
PyLGTVCmdException,
diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py
index ece76b5ed32..34277eb3c09 100644
--- a/homeassistant/components/webostv/notify.py
+++ b/homeassistant/components/webostv/notify.py
@@ -55,7 +55,6 @@ class LgWebOSNotificationService(BaseNotificationService):
except (
OSError,
ConnectionClosed,
- ConnectionRefusedError,
asyncio.TimeoutError,
asyncio.CancelledError,
PyLGTVCmdException,
diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py
index cb3beb9b67a..9e9aa5ee278 100644
--- a/homeassistant/components/wemo/__init__.py
+++ b/homeassistant/components/wemo/__init__.py
@@ -20,6 +20,7 @@ from homeassistant.helpers.event import async_call_later
from homeassistant.util.async_ import gather_with_concurrency
from .const import DOMAIN
+from .wemo_device import async_register_device
# Max number of devices to initialize at once. This limit is in place to
# avoid tying up too many executor threads with WeMo device setup.
@@ -98,13 +99,18 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a wemo config entry."""
config = hass.data[DOMAIN].pop("config")
# Keep track of WeMo device subscriptions for push updates
registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry()
await hass.async_add_executor_job(registry.start)
+
+ # Respond to discovery requests from WeMo devices.
+ discovery_responder = pywemo.ssdp.DiscoveryResponder(registry.port)
+ await hass.async_add_executor_job(discovery_responder.start)
+
static_conf = config.get(CONF_STATIC, [])
wemo_dispatcher = WemoDispatcher(entry)
wemo_discovery = WemoDiscovery(hass, wemo_dispatcher, static_conf)
@@ -113,6 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Shutdown Wemo subscriptions and subscription thread on exit."""
_LOGGER.debug("Shutting down WeMo event subscriptions")
await hass.async_add_executor_job(registry.stop)
+ await hass.async_add_executor_job(discovery_responder.stop)
wemo_discovery.async_stop_discovery()
entry.async_on_unload(
@@ -137,15 +144,15 @@ class WemoDispatcher:
self._added_serial_numbers = set()
self._loaded_components = set()
- @callback
- def async_add_unique_device(
- self, hass: HomeAssistant, device: pywemo.WeMoDevice
+ async def async_add_unique_device(
+ self, hass: HomeAssistant, wemo: pywemo.WeMoDevice
) -> None:
"""Add a WeMo device to hass if it has not already been added."""
- if device.serialnumber in self._added_serial_numbers:
+ if wemo.serialnumber in self._added_serial_numbers:
return
- component = WEMO_MODEL_DISPATCH.get(device.model_name, SWITCH_DOMAIN)
+ component = WEMO_MODEL_DISPATCH.get(wemo.model_name, SWITCH_DOMAIN)
+ device = await async_register_device(hass, self._config_entry, wemo)
# Three cases:
# - First time we see component, we need to load it and initialize the backlog
@@ -171,7 +178,7 @@ class WemoDispatcher:
device,
)
- self._added_serial_numbers.add(device.serialnumber)
+ self._added_serial_numbers.add(wemo.serialnumber)
class WemoDiscovery:
@@ -200,7 +207,7 @@ class WemoDiscovery:
for device in await self._hass.async_add_executor_job(
pywemo.discover_devices
):
- self._wemo_dispatcher.async_add_unique_device(self._hass, device)
+ await self._wemo_dispatcher.async_add_unique_device(self._hass, device)
await self.discover_statics()
finally:
@@ -236,7 +243,9 @@ class WemoDiscovery:
],
):
if device:
- self._wemo_dispatcher.async_add_unique_device(self._hass, device)
+ await self._wemo_dispatcher.async_add_unique_device(
+ self._hass, device
+ )
def validate_static_config(host, port):
diff --git a/homeassistant/components/wemo/const.py b/homeassistant/components/wemo/const.py
index e9272d39bdd..79972affa48 100644
--- a/homeassistant/components/wemo/const.py
+++ b/homeassistant/components/wemo/const.py
@@ -3,3 +3,6 @@ DOMAIN = "wemo"
SERVICE_SET_HUMIDITY = "set_humidity"
SERVICE_RESET_FILTER_LIFE = "reset_filter_life"
+SIGNAL_WEMO_STATE_PUSH = f"{DOMAIN}.state_push"
+
+WEMO_SUBSCRIPTION_EVENT = f"{DOMAIN}_subscription_event"
diff --git a/homeassistant/components/wemo/device_trigger.py b/homeassistant/components/wemo/device_trigger.py
new file mode 100644
index 00000000000..ba2ac08ed74
--- /dev/null
+++ b/homeassistant/components/wemo/device_trigger.py
@@ -0,0 +1,61 @@
+"""Triggers for WeMo devices."""
+from pywemo.subscribe import EVENT_TYPE_LONG_PRESS
+import voluptuous as vol
+
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
+from homeassistant.components.homeassistant.triggers import event as event_trigger
+from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
+
+from .const import DOMAIN as WEMO_DOMAIN, WEMO_SUBSCRIPTION_EVENT
+from .wemo_device import async_get_device
+
+TRIGGER_TYPES = {EVENT_TYPE_LONG_PRESS}
+
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
+ }
+)
+
+
+async def async_get_triggers(hass, device_id):
+ """Return a list of triggers."""
+
+ wemo_trigger = {
+ # Required fields of TRIGGER_BASE_SCHEMA
+ CONF_PLATFORM: "device",
+ CONF_DOMAIN: WEMO_DOMAIN,
+ CONF_DEVICE_ID: device_id,
+ }
+
+ device = async_get_device(hass, device_id)
+ triggers = []
+
+ # Check for long press support.
+ if device.supports_long_press:
+ triggers.append(
+ {
+ # Required fields of TRIGGER_SCHEMA
+ CONF_TYPE: EVENT_TYPE_LONG_PRESS,
+ **wemo_trigger,
+ }
+ )
+
+ return triggers
+
+
+async def async_attach_trigger(hass, config, action, automation_info):
+ """Attach a trigger."""
+ event_config = event_trigger.TRIGGER_SCHEMA(
+ {
+ event_trigger.CONF_PLATFORM: "event",
+ event_trigger.CONF_EVENT_TYPE: WEMO_SUBSCRIPTION_EVENT,
+ event_trigger.CONF_EVENT_DATA: {
+ CONF_DEVICE_ID: config[CONF_DEVICE_ID],
+ CONF_TYPE: config[CONF_TYPE],
+ },
+ }
+ )
+ return await event_trigger.async_attach_trigger(
+ hass, event_config, action, automation_info, platform_type="device"
+ )
diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py
index 810ad74b953..19035367ae5 100644
--- a/homeassistant/components/wemo/entity.py
+++ b/homeassistant/components/wemo/entity.py
@@ -10,9 +10,11 @@ import async_timeout
from pywemo import WeMoDevice
from pywemo.exceptions import ActionException
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity
-from .const import DOMAIN as WEMO_DOMAIN
+from .const import DOMAIN as WEMO_DOMAIN, SIGNAL_WEMO_STATE_PUSH
+from .wemo_device import DeviceWrapper
_LOGGER = logging.getLogger(__name__)
@@ -35,9 +37,9 @@ class WemoEntity(Entity):
Requires that subclasses implement the _update method.
"""
- def __init__(self, device: WeMoDevice) -> None:
+ def __init__(self, wemo: WeMoDevice) -> None:
"""Initialize the WeMo device."""
- self.wemo = device
+ self.wemo = wemo
self._state = None
self._available = True
self._update_lock = None
@@ -120,6 +122,12 @@ class WemoEntity(Entity):
class WemoSubscriptionEntity(WemoEntity):
"""Common methods for Wemo devices that register for update callbacks."""
+ def __init__(self, device: DeviceWrapper) -> None:
+ """Initialize WemoSubscriptionEntity."""
+ super().__init__(device.wemo)
+ self._device_id = device.device_id
+ self._device_info = device.device_info
+
@property
def unique_id(self) -> str:
"""Return the id of this WeMo device."""
@@ -128,12 +136,7 @@ class WemoSubscriptionEntity(WemoEntity):
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
- return {
- "name": self.name,
- "identifiers": {(WEMO_DOMAIN, self.unique_id)},
- "model": self.wemo.model_name,
- "manufacturer": "Belkin",
- }
+ return self._device_info
@property
def is_on(self) -> bool:
@@ -169,27 +172,25 @@ class WemoSubscriptionEntity(WemoEntity):
"""Wemo device added to Home Assistant."""
await super().async_added_to_hass()
- registry = self.hass.data[WEMO_DOMAIN]["registry"]
- await self.hass.async_add_executor_job(registry.register, self.wemo)
- registry.on(self.wemo, None, self._subscription_callback)
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass, SIGNAL_WEMO_STATE_PUSH, self._async_subscription_callback
+ )
+ )
- async def async_will_remove_from_hass(self) -> None:
- """Wemo device removed from hass."""
- registry = self.hass.data[WEMO_DOMAIN]["registry"]
- await self.hass.async_add_executor_job(registry.unregister, self.wemo)
-
- def _subscription_callback(
- self, _device: WeMoDevice, _type: str, _params: str
+ async def _async_subscription_callback(
+ self, device_id: str, event_type: str, params: str
) -> None:
"""Update the state by the Wemo device."""
- _LOGGER.info("Subscription update for %s", self.name)
- updated = self.wemo.subscription_update(_type, _params)
- self.hass.add_job(self._async_locked_subscription_callback(not updated))
-
- async def _async_locked_subscription_callback(self, force_update: bool) -> None:
- """Handle an update from a subscription."""
+ # Only respond events for this device.
+ if device_id != self._device_id:
+ return
# If an update is in progress, we don't do anything
if self._update_lock.locked():
return
- await self._async_locked_update(force_update)
+ _LOGGER.debug("Subscription event (%s) for %s", event_type, self.name)
+ updated = await self.hass.async_add_executor_job(
+ self.wemo.subscription_update, event_type, params
+ )
+ await self._async_locked_update(not updated)
diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py
index bbcdafaf351..79f2e9b7172 100644
--- a/homeassistant/components/wemo/light.py
+++ b/homeassistant/components/wemo/light.py
@@ -40,11 +40,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _discovered_wemo(device):
"""Handle a discovered Wemo device."""
- if device.model_name == "Dimmer":
+ if device.wemo.model_name == "Dimmer":
async_add_entities([WemoDimmer(device)])
else:
await hass.async_add_executor_job(
- setup_bridge, hass, device, async_add_entities
+ setup_bridge, hass, device.wemo, async_add_entities
)
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.light", _discovered_wemo)
diff --git a/homeassistant/components/wemo/strings.json b/homeassistant/components/wemo/strings.json
index f7c6329b1af..3419b2cb3d1 100644
--- a/homeassistant/components/wemo/strings.json
+++ b/homeassistant/components/wemo/strings.json
@@ -9,5 +9,10 @@
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "long_press": "Wemo button was pressed for 2 seconds"
+ }
}
}
diff --git a/homeassistant/components/wemo/translations/ca.json b/homeassistant/components/wemo/translations/ca.json
index 1216504eb85..ec27622fa83 100644
--- a/homeassistant/components/wemo/translations/ca.json
+++ b/homeassistant/components/wemo/translations/ca.json
@@ -9,5 +9,10 @@
"description": "Vols configurar Wemo?"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "long_press": "Bot\u00f3 Wemo premut durant 2 segons"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/de.json b/homeassistant/components/wemo/translations/de.json
index 81694f65ea2..b0735db1249 100644
--- a/homeassistant/components/wemo/translations/de.json
+++ b/homeassistant/components/wemo/translations/de.json
@@ -9,5 +9,10 @@
"description": "M\u00f6chtest du Wemo einrichten?"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "long_press": "Wemo-Taste wurde 2 Sekunden lang gedr\u00fcckt"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/en.json b/homeassistant/components/wemo/translations/en.json
index 20a9af468ff..e3c2b18c2ad 100644
--- a/homeassistant/components/wemo/translations/en.json
+++ b/homeassistant/components/wemo/translations/en.json
@@ -9,5 +9,10 @@
"description": "Do you want to set up Wemo?"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "long_press": "Wemo button was pressed for 2 seconds"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/es.json b/homeassistant/components/wemo/translations/es.json
index 1c7088a5280..4c176762d04 100644
--- a/homeassistant/components/wemo/translations/es.json
+++ b/homeassistant/components/wemo/translations/es.json
@@ -9,5 +9,10 @@
"description": "\u00bfQuieres configurar Wemo?"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "long_press": "Se ha pulsado el bot\u00f3n Wemo durante 2 segundos"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/et.json b/homeassistant/components/wemo/translations/et.json
index e9959620510..d99f6bc1f57 100644
--- a/homeassistant/components/wemo/translations/et.json
+++ b/homeassistant/components/wemo/translations/et.json
@@ -9,5 +9,10 @@
"description": "Kas soovid Wemo-t seadistada?"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "long_press": "Wemo nuppu vajutati 2 sekundit"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/he.json b/homeassistant/components/wemo/translations/he.json
new file mode 100644
index 00000000000..380dbc5d7fc
--- /dev/null
+++ b/homeassistant/components/wemo/translations/he.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/it.json b/homeassistant/components/wemo/translations/it.json
index 691c7ca46b3..c1c66ac0ded 100644
--- a/homeassistant/components/wemo/translations/it.json
+++ b/homeassistant/components/wemo/translations/it.json
@@ -9,5 +9,10 @@
"description": "Vuoi configurare Wemo?"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "long_press": "Il pulsante Wemo \u00e8 stato premuto per 2 secondi"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/nl.json b/homeassistant/components/wemo/translations/nl.json
index e4087863726..7fde5a7ef42 100644
--- a/homeassistant/components/wemo/translations/nl.json
+++ b/homeassistant/components/wemo/translations/nl.json
@@ -9,5 +9,10 @@
"description": "Wilt u Wemo instellen?"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "long_press": "Wemo-knop werd 2 seconden ingedrukt"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/no.json b/homeassistant/components/wemo/translations/no.json
index 59958ac048d..c5ab2233b68 100644
--- a/homeassistant/components/wemo/translations/no.json
+++ b/homeassistant/components/wemo/translations/no.json
@@ -9,5 +9,10 @@
"description": "\u00d8nsker du \u00e5 sette opp Wemo?"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "long_press": "Wemo-knappen ble trykket i 2 sekunder"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/pl.json b/homeassistant/components/wemo/translations/pl.json
index a8ee3fa57ac..d8b06f79e48 100644
--- a/homeassistant/components/wemo/translations/pl.json
+++ b/homeassistant/components/wemo/translations/pl.json
@@ -9,5 +9,10 @@
"description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "long_press": "Przycisk Wemo zosta\u0142 wci\u015bni\u0119ty przez 2 sekundy"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/ru.json b/homeassistant/components/wemo/translations/ru.json
index 60b1efb3dea..91994b113de 100644
--- a/homeassistant/components/wemo/translations/ru.json
+++ b/homeassistant/components/wemo/translations/ru.json
@@ -9,5 +9,10 @@
"description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Wemo?"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 Wemo \u0431\u044b\u043b\u0430 \u043d\u0430\u0436\u0430\u0442\u0430 \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 2 \u0441\u0435\u043a\u0443\u043d\u0434"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/zh-Hant.json b/homeassistant/components/wemo/translations/zh-Hant.json
index 4be83508478..a9a4a2a8b20 100644
--- a/homeassistant/components/wemo/translations/zh-Hant.json
+++ b/homeassistant/components/wemo/translations/zh-Hant.json
@@ -9,5 +9,10 @@
"description": "\u662f\u5426\u8981\u8a2d\u5b9a Wemo\uff1f"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "long_press": "Wemo \u6309\u9215\u6309\u4e0b 2 \u79d2"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py
new file mode 100644
index 00000000000..3b0fbdcbe55
--- /dev/null
+++ b/homeassistant/components/wemo/wemo_device.py
@@ -0,0 +1,96 @@
+"""Home Assistant wrapper for a pyWeMo device."""
+import logging
+
+from pywemo import PyWeMoException, WeMoDevice
+from pywemo.subscribe import EVENT_TYPE_LONG_PRESS
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ CONF_DEVICE_ID,
+ CONF_NAME,
+ CONF_PARAMS,
+ CONF_TYPE,
+ CONF_UNIQUE_ID,
+)
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.device_registry import async_get as async_get_device_registry
+from homeassistant.helpers.dispatcher import dispatcher_send
+
+from .const import DOMAIN, SIGNAL_WEMO_STATE_PUSH, WEMO_SUBSCRIPTION_EVENT
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class DeviceWrapper:
+ """Home Assistant wrapper for a pyWeMo device."""
+
+ def __init__(self, hass: HomeAssistant, wemo: WeMoDevice, device_id: str) -> None:
+ """Initialize DeviceWrapper."""
+ self.hass = hass
+ self.wemo = wemo
+ self.device_id = device_id
+ self.device_info = _device_info(wemo)
+ self.supports_long_press = wemo.supports_long_press()
+
+ def subscription_callback(
+ self, _device: WeMoDevice, event_type: str, params: str
+ ) -> None:
+ """Receives push notifications from WeMo devices."""
+ if event_type == EVENT_TYPE_LONG_PRESS:
+ self.hass.bus.fire(
+ WEMO_SUBSCRIPTION_EVENT,
+ {
+ CONF_DEVICE_ID: self.device_id,
+ CONF_NAME: self.wemo.name,
+ CONF_TYPE: event_type,
+ CONF_PARAMS: params,
+ CONF_UNIQUE_ID: self.wemo.serialnumber,
+ },
+ )
+ else:
+ dispatcher_send(
+ self.hass, SIGNAL_WEMO_STATE_PUSH, self.device_id, event_type, params
+ )
+
+
+def _device_info(wemo: WeMoDevice):
+ return {
+ "name": wemo.name,
+ "identifiers": {(DOMAIN, wemo.serialnumber)},
+ "model": wemo.model_name,
+ "manufacturer": "Belkin",
+ }
+
+
+async def async_register_device(
+ hass: HomeAssistant, config_entry: ConfigEntry, wemo: WeMoDevice
+) -> DeviceWrapper:
+ """Register a device with home assistant and enable pywemo event callbacks."""
+ device_registry = async_get_device_registry(hass)
+ entry = device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id, **_device_info(wemo)
+ )
+
+ registry = hass.data[DOMAIN]["registry"]
+ await hass.async_add_executor_job(registry.register, wemo)
+
+ device = DeviceWrapper(hass, wemo, entry.id)
+ hass.data[DOMAIN].setdefault("devices", {})[entry.id] = device
+ registry.on(wemo, None, device.subscription_callback)
+
+ if device.supports_long_press:
+ try:
+ await hass.async_add_executor_job(wemo.ensure_long_press_virtual_device)
+ except PyWeMoException:
+ _LOGGER.warning(
+ "Failed to enable long press support for device: %s", wemo.name
+ )
+ device.supports_long_press = False
+
+ return device
+
+
+@callback
+def async_get_device(hass: HomeAssistant, device_id: str) -> DeviceWrapper:
+ """Return DeviceWrapper for device_id."""
+ return hass.data[DOMAIN]["devices"][device_id]
diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py
index f36e4b0df32..55b13921c1c 100644
--- a/homeassistant/components/wiffi/__init__.py
+++ b/homeassistant/components/wiffi/__init__.py
@@ -32,17 +32,17 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor", "binary_sensor"]
-async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up wiffi from a config entry, config_entry contains data from config entry database."""
- if not config_entry.update_listeners:
- config_entry.add_update_listener(async_update_options)
+ if not entry.update_listeners:
+ entry.add_update_listener(async_update_options)
# create api object
api = WiffiIntegrationApi(hass)
- api.async_setup(config_entry)
+ api.async_setup(entry)
# store api object
- hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = api
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api
try:
await api.server.start_server()
@@ -50,29 +50,27 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
if exc.errno != errno.EADDRINUSE:
_LOGGER.error("Start_server failed, errno: %d", exc.errno)
return False
- _LOGGER.error("Port %s already in use", config_entry.data[CONF_PORT])
+ _LOGGER.error("Port %s already in use", entry.data[CONF_PORT])
raise ConfigEntryNotReady from exc
- hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
-async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_update_options(hass: HomeAssistant, entry: ConfigEntry):
"""Update options."""
- await hass.config_entries.async_reload(config_entry.entry_id)
+ await hass.config_entries.async_reload(entry.entry_id)
-async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
- api: WiffiIntegrationApi = hass.data[DOMAIN][config_entry.entry_id]
+ api: WiffiIntegrationApi = hass.data[DOMAIN][entry.entry_id]
await api.server.close_server()
- unload_ok = await hass.config_entries.async_unload_platforms(
- config_entry, PLATFORMS
- )
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
- api = hass.data[DOMAIN].pop(config_entry.entry_id)
+ api = hass.data[DOMAIN].pop(entry.entry_id)
api.shutdown()
return unload_ok
diff --git a/homeassistant/components/wiffi/translations/he.json b/homeassistant/components/wiffi/translations/he.json
new file mode 100644
index 00000000000..048132fe5b4
--- /dev/null
+++ b/homeassistant/components/wiffi/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "port": "\u05e4\u05ea\u05d7\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py
index 0ac2713994b..3c3c24db793 100644
--- a/homeassistant/components/wilight/__init__.py
+++ b/homeassistant/components/wilight/__init__.py
@@ -13,7 +13,7 @@ DOMAIN = "wilight"
PLATFORMS = ["cover", "fan", "light"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a wilight config entry."""
parent = WiLightParent(hass, entry)
diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json
index 59838d9ee86..851defe0e32 100644
--- a/homeassistant/components/wilight/translations/de.json
+++ b/homeassistant/components/wilight/translations/de.json
@@ -1,9 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "not_supported_device": "Dieses WiLight wird derzeit nicht unterst\u00fctzt",
+ "not_wilight_device": "Dieses Ger\u00e4t ist kein WiLight"
},
- "flow_title": "WiLight: {name}",
+ "flow_title": "{name}",
"step": {
"confirm": {
"description": "M\u00f6chten Sie WiLight {name} einrichten? \n\n Es unterst\u00fctzt: {components}",
diff --git a/homeassistant/components/wilight/translations/he.json b/homeassistant/components/wilight/translations/he.json
new file mode 100644
index 00000000000..977167ec765
--- /dev/null
+++ b/homeassistant/components/wilight/translations/he.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm": {
+ "title": "WiLight"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wilight/translations/hu.json b/homeassistant/components/wilight/translations/hu.json
index 26cdaf6a025..4ddb08bb975 100644
--- a/homeassistant/components/wilight/translations/hu.json
+++ b/homeassistant/components/wilight/translations/hu.json
@@ -3,6 +3,7 @@
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
},
+ "flow_title": "{name}",
"step": {
"confirm": {
"title": "WiLight"
diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py
index ecb52530d7e..25ed695e8ff 100644
--- a/homeassistant/components/withings/binary_sensor.py
+++ b/homeassistant/components/withings/binary_sensor.py
@@ -29,12 +29,9 @@ async def async_setup_entry(
class WithingsHealthBinarySensor(BaseWithingsSensor, BinarySensorEntity):
"""Implementation of a Withings sensor."""
+ _attr_device_class = DEVICE_CLASS_OCCUPANCY
+
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self._state_data
-
- @property
- def device_class(self) -> str:
- """Provide the device class."""
- return DEVICE_CLASS_OCCUPANCY
diff --git a/homeassistant/components/withings/translations/de.json b/homeassistant/components/withings/translations/de.json
index 7d0aead7a66..31d5ad2f6e5 100644
--- a/homeassistant/components/withings/translations/de.json
+++ b/homeassistant/components/withings/translations/de.json
@@ -12,7 +12,7 @@
"error": {
"already_configured": "Konto wurde bereits konfiguriert"
},
- "flow_title": "Withings: {profile}",
+ "flow_title": "{profile}",
"step": {
"pick_implementation": {
"title": "W\u00e4hle die Authentifizierungsmethode"
diff --git a/homeassistant/components/withings/translations/he.json b/homeassistant/components/withings/translations/he.json
new file mode 100644
index 00000000000..6bcd8bf9a9d
--- /dev/null
+++ b/homeassistant/components/withings/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+ "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})"
+ },
+ "error": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "flow_title": "{profile}",
+ "step": {
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ },
+ "reauth": {
+ "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json
index d39410b6d17..ec8c628a485 100644
--- a/homeassistant/components/withings/translations/hu.json
+++ b/homeassistant/components/withings/translations/hu.json
@@ -12,6 +12,7 @@
"error": {
"already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
},
+ "flow_title": "{profile}",
"step": {
"pick_implementation": {
"title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py
index b44df82f889..29c6b98b381 100644
--- a/homeassistant/components/wled/__init__.py
+++ b/homeassistant/components/wled/__init__.py
@@ -1,45 +1,22 @@
"""Support for WLED."""
from __future__ import annotations
-from datetime import timedelta
-import logging
-
-from wled import WLED, Device as WLEDDevice, WLEDConnectionError, WLEDError
-
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
+from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import (
- ATTR_IDENTIFIERS,
- ATTR_MANUFACTURER,
- ATTR_MODEL,
- ATTR_NAME,
- ATTR_SW_VERSION,
- CONF_HOST,
-)
from homeassistant.core import HomeAssistant
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.entity import DeviceInfo
-from homeassistant.helpers.update_coordinator import (
- CoordinatorEntity,
- DataUpdateCoordinator,
- UpdateFailed,
-)
from .const import DOMAIN
+from .coordinator import WLEDDataUpdateCoordinator
-SCAN_INTERVAL = timedelta(seconds=5)
-PLATFORMS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN)
-
-_LOGGER = logging.getLogger(__name__)
+PLATFORMS = (LIGHT_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up WLED from a config entry."""
-
- # Create WLED instance for this entry
- coordinator = WLEDDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
+ coordinator = WLEDDataUpdateCoordinator(hass, entry=entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
@@ -54,90 +31,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Set up all platforms for this device/entry.
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+ # Reload entry when its updated.
+ entry.async_on_unload(entry.add_update_listener(async_reload_entry))
+
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload WLED config entry."""
-
- # Unload entities for this entry/device.
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
- del hass.data[DOMAIN][entry.entry_id]
+ coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
- if not hass.data[DOMAIN]:
- del hass.data[DOMAIN]
+ # Ensure disconnected and cleanup stop sub
+ await coordinator.wled.disconnect()
+ if coordinator.unsub:
+ coordinator.unsub()
+
+ del hass.data[DOMAIN][entry.entry_id]
return unload_ok
-def wled_exception_handler(func):
- """Decorate WLED calls to handle WLED exceptions.
-
- A decorator that wraps the passed in function, catches WLED errors,
- and handles the availability of the device in the data coordinator.
- """
-
- async def handler(self, *args, **kwargs):
- try:
- await func(self, *args, **kwargs)
- self.coordinator.update_listeners()
-
- except WLEDConnectionError as error:
- _LOGGER.error("Error communicating with API: %s", error)
- self.coordinator.last_update_success = False
- self.coordinator.update_listeners()
-
- except WLEDError as error:
- _LOGGER.error("Invalid response from API: %s", error)
-
- return handler
-
-
-class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]):
- """Class to manage fetching WLED data from single endpoint."""
-
- def __init__(
- self,
- hass: HomeAssistant,
- *,
- host: str,
- ) -> None:
- """Initialize global WLED data updater."""
- self.wled = WLED(host, session=async_get_clientsession(hass))
-
- super().__init__(
- hass,
- _LOGGER,
- name=DOMAIN,
- update_interval=SCAN_INTERVAL,
- )
-
- def update_listeners(self) -> None:
- """Call update on all listeners."""
- for update_callback in self._listeners:
- update_callback()
-
- async def _async_update_data(self) -> WLEDDevice:
- """Fetch data from WLED."""
- try:
- return await self.wled.update(full_update=not self.last_update_success)
- except WLEDError as error:
- raise UpdateFailed(f"Invalid response from API: {error}") from error
-
-
-class WLEDEntity(CoordinatorEntity):
- """Defines a base WLED entity."""
-
- coordinator: WLEDDataUpdateCoordinator
-
- @property
- def device_info(self) -> DeviceInfo:
- """Return device information about this WLED device."""
- return {
- ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)},
- ATTR_NAME: self.coordinator.data.info.name,
- ATTR_MANUFACTURER: self.coordinator.data.info.brand,
- ATTR_MODEL: self.coordinator.data.info.product,
- ATTR_SW_VERSION: self.coordinator.data.info.version,
- }
+async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
+ """Reload the config entry when it changed."""
+ await hass.config_entries.async_reload(entry.entry_id)
diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py
index bb9d4c0cfe5..7f4d006d122 100644
--- a/homeassistant/components/wled/config_flow.py
+++ b/homeassistant/components/wled/config_flow.py
@@ -6,13 +6,19 @@ from typing import Any
import voluptuous as vol
from wled import WLED, WLEDConnectionError
-from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow
+from homeassistant.config_entries import (
+ SOURCE_ZEROCONF,
+ ConfigEntry,
+ ConfigFlow,
+ OptionsFlow,
+)
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
+from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import DiscoveryInfoType
-from .const import DOMAIN
+from .const import CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT, DOMAIN
class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -20,6 +26,12 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry: ConfigEntry) -> WLEDOptionsFlowHandler:
+ """Get the options flow for this handler."""
+ return WLEDOptionsFlowHandler(config_entry)
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -115,3 +127,32 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
description_placeholders={"name": name},
errors=errors or {},
)
+
+
+class WLEDOptionsFlowHandler(OptionsFlow):
+ """Handle WLED options."""
+
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize WLED options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Manage WLED options."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ return self.async_show_form(
+ step_id="init",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_KEEP_MASTER_LIGHT,
+ default=self.config_entry.options.get(
+ CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT
+ ),
+ ): bool,
+ }
+ ),
+ )
diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py
index 7cc52601d79..d80dbf16a60 100644
--- a/homeassistant/components/wled/const.py
+++ b/homeassistant/components/wled/const.py
@@ -1,8 +1,17 @@
"""Constants for the WLED integration."""
+from datetime import timedelta
+import logging
# Integration domain
DOMAIN = "wled"
+LOGGER = logging.getLogger(__package__)
+SCAN_INTERVAL = timedelta(seconds=10)
+
+# Options
+CONF_KEEP_MASTER_LIGHT = "keep_master_light"
+DEFAULT_KEEP_MASTER_LIGHT = False
+
# Attributes
ATTR_COLOR_PRIMARY = "color_primary"
ATTR_DURATION = "duration"
diff --git a/homeassistant/components/wled/coordinator.py b/homeassistant/components/wled/coordinator.py
new file mode 100644
index 00000000000..b730ac1543a
--- /dev/null
+++ b/homeassistant/components/wled/coordinator.py
@@ -0,0 +1,119 @@
+"""DataUpdateCoordinator for WLED."""
+from __future__ import annotations
+
+import asyncio
+from typing import Callable
+
+from wled import WLED, Device as WLEDDevice, WLEDConnectionClosed, WLEDError
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import (
+ CONF_KEEP_MASTER_LIGHT,
+ DEFAULT_KEEP_MASTER_LIGHT,
+ DOMAIN,
+ LOGGER,
+ SCAN_INTERVAL,
+)
+
+
+class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]):
+ """Class to manage fetching WLED data from single endpoint."""
+
+ keep_master_light: bool
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ *,
+ entry: ConfigEntry,
+ ) -> None:
+ """Initialize global WLED data updater."""
+ self.keep_master_light = entry.options.get(
+ CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT
+ )
+ self.wled = WLED(entry.data[CONF_HOST], session=async_get_clientsession(hass))
+ self.unsub: Callable | None = None
+
+ super().__init__(
+ hass,
+ LOGGER,
+ name=DOMAIN,
+ update_interval=SCAN_INTERVAL,
+ )
+
+ @property
+ def has_master_light(self) -> bool:
+ """Return if the coordinated device has an master light."""
+ return self.keep_master_light or (
+ self.data is not None and len(self.data.state.segments) > 1
+ )
+
+ def update_listeners(self) -> None:
+ """Call update on all listeners."""
+ for update_callback in self._listeners:
+ update_callback()
+
+ @callback
+ def _use_websocket(self) -> None:
+ """Use WebSocket for updates, instead of polling."""
+
+ async def listen() -> None:
+ """Listen for state changes via WebSocket."""
+ try:
+ await self.wled.connect()
+ except WLEDError as err:
+ self.logger.info(err)
+ if self.unsub:
+ self.unsub()
+ self.unsub = None
+ return
+
+ try:
+ await self.wled.listen(callback=self.async_set_updated_data)
+ except WLEDConnectionClosed as err:
+ self.last_update_success = False
+ self.logger.info(err)
+ except WLEDError as err:
+ self.last_update_success = False
+ self.update_listeners()
+ self.logger.error(err)
+
+ # Ensure we are disconnected
+ await self.wled.disconnect()
+ if self.unsub:
+ self.unsub()
+ self.unsub = None
+
+ async def close_websocket(_) -> None:
+ """Close WebSocket connection."""
+ await self.wled.disconnect()
+
+ # Clean disconnect WebSocket on Home Assistant shutdown
+ self.unsub = self.hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP, close_websocket
+ )
+
+ # Start listening
+ asyncio.create_task(listen())
+
+ async def _async_update_data(self) -> WLEDDevice:
+ """Fetch data from WLED."""
+ try:
+ device = await self.wled.update(full_update=not self.last_update_success)
+ except WLEDError as error:
+ raise UpdateFailed(f"Invalid response from API: {error}") from error
+
+ # If the device supports a WebSocket, try activating it.
+ if (
+ device.info.websocket is not None
+ and not self.wled.connected
+ and not self.unsub
+ ):
+ self._use_websocket()
+
+ return device
diff --git a/homeassistant/components/wled/helpers.py b/homeassistant/components/wled/helpers.py
new file mode 100644
index 00000000000..d5ca895390b
--- /dev/null
+++ b/homeassistant/components/wled/helpers.py
@@ -0,0 +1,28 @@
+"""Helpers for WLED."""
+
+from wled import WLEDConnectionError, WLEDError
+
+from .const import LOGGER
+
+
+def wled_exception_handler(func):
+ """Decorate WLED calls to handle WLED exceptions.
+
+ A decorator that wraps the passed in function, catches WLED errors,
+ and handles the availability of the device in the data coordinator.
+ """
+
+ async def handler(self, *args, **kwargs):
+ try:
+ await func(self, *args, **kwargs)
+ self.coordinator.update_listeners()
+
+ except WLEDConnectionError as error:
+ LOGGER.error("Error communicating with API: %s", error)
+ self.coordinator.last_update_success = False
+ self.coordinator.update_listeners()
+
+ except WLEDError as error:
+ LOGGER.error("Invalid response from API: %s", error)
+
+ return handler
diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py
index 20960ad0bb7..4326f1066c7 100644
--- a/homeassistant/components/wled/light.py
+++ b/homeassistant/components/wled/light.py
@@ -2,35 +2,28 @@
from __future__ import annotations
from functools import partial
-from typing import Any
+from typing import Any, Tuple, cast
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
- ATTR_COLOR_TEMP,
ATTR_EFFECT,
- ATTR_HS_COLOR,
+ ATTR_RGB_COLOR,
+ ATTR_RGBW_COLOR,
ATTR_TRANSITION,
- ATTR_WHITE_VALUE,
- SUPPORT_BRIGHTNESS,
- SUPPORT_COLOR,
- SUPPORT_COLOR_TEMP,
+ COLOR_MODE_BRIGHTNESS,
+ COLOR_MODE_RGB,
+ COLOR_MODE_RGBW,
SUPPORT_EFFECT,
SUPPORT_TRANSITION,
- SUPPORT_WHITE_VALUE,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.entity_registry import (
- async_get_registry as async_get_entity_registry,
-)
-import homeassistant.util.color as color_util
-from . import WLEDDataUpdateCoordinator, WLEDEntity, wled_exception_handler
from .const import (
ATTR_COLOR_PRIMARY,
ATTR_INTENSITY,
@@ -45,6 +38,9 @@ from .const import (
SERVICE_EFFECT,
SERVICE_PRESET,
)
+from .coordinator import WLEDDataUpdateCoordinator
+from .helpers import wled_exception_handler
+from .models import WLEDEntity
PARALLEL_UPDATES = 1
@@ -85,8 +81,14 @@ async def async_setup_entry(
"async_preset",
)
+ if coordinator.keep_master_light:
+ async_add_entities([WLEDMasterLight(coordinator=coordinator)])
+
update_segments = partial(
- async_update_segments, entry, coordinator, {}, async_add_entities
+ async_update_segments,
+ coordinator,
+ set(),
+ async_add_entities,
)
coordinator.async_add_listener(update_segments)
@@ -96,14 +98,16 @@ async def async_setup_entry(
class WLEDMasterLight(WLEDEntity, LightEntity):
"""Defines a WLED master light."""
- _attr_supported_features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
+ _attr_color_mode = COLOR_MODE_BRIGHTNESS
_attr_icon = "mdi:led-strip-variant"
+ _attr_supported_features = SUPPORT_TRANSITION
def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None:
"""Initialize WLED master light."""
super().__init__(coordinator=coordinator)
self._attr_name = f"{coordinator.data.info.name} Master"
self._attr_unique_id = coordinator.data.info.mac_address
+ self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS}
@property
def brightness(self) -> int | None:
@@ -115,30 +119,32 @@ class WLEDMasterLight(WLEDEntity, LightEntity):
"""Return the state of the light."""
return bool(self.coordinator.data.state.on)
+ @property
+ def available(self) -> bool:
+ """Return if this master light is available or not."""
+ return self.coordinator.has_master_light and super().available
+
@wled_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
- data: dict[str, bool | int] = {ATTR_ON: False}
-
+ transition = None
if ATTR_TRANSITION in kwargs:
# WLED uses 100ms per unit, so 10 = 1 second.
- data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10)
+ transition = round(kwargs[ATTR_TRANSITION] * 10)
- await self.coordinator.wled.master(**data)
+ await self.coordinator.wled.master(on=False, transition=transition)
@wled_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
- data: dict[str, bool | int] = {ATTR_ON: True}
-
+ transition = None
if ATTR_TRANSITION in kwargs:
# WLED uses 100ms per unit, so 10 = 1 second.
- data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10)
+ transition = round(kwargs[ATTR_TRANSITION] * 10)
- if ATTR_BRIGHTNESS in kwargs:
- data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS]
-
- await self.coordinator.wled.master(**data)
+ await self.coordinator.wled.master(
+ on=True, brightness=kwargs.get(ATTR_BRIGHTNESS), transition=transition
+ )
async def async_effect(
self,
@@ -157,31 +163,42 @@ class WLEDMasterLight(WLEDEntity, LightEntity):
preset: int,
) -> None:
"""Set a WLED light to a saved preset."""
- data = {ATTR_PRESET: preset}
-
- await self.coordinator.wled.preset(**data)
+ await self.coordinator.wled.preset(preset=preset)
class WLEDSegmentLight(WLEDEntity, LightEntity):
"""Defines a WLED light based on a segment."""
+ _attr_supported_features = SUPPORT_EFFECT | SUPPORT_TRANSITION
_attr_icon = "mdi:led-strip-variant"
- def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None:
+ def __init__(
+ self,
+ coordinator: WLEDDataUpdateCoordinator,
+ segment: int,
+ ) -> None:
"""Initialize WLED segment light."""
super().__init__(coordinator=coordinator)
self._rgbw = coordinator.data.info.leds.rgbw
+ self._wv = coordinator.data.info.leds.wv
self._segment = segment
- # If this is the one and only segment, use a simpler name
+ # Segment 0 uses a simpler name, which is more natural for when using
+ # a single segment / using WLED with one big LED strip.
self._attr_name = f"{coordinator.data.info.name} Segment {segment}"
- if len(coordinator.data.state.segments) == 1:
+ if segment == 0:
self._attr_name = coordinator.data.info.name
self._attr_unique_id = (
f"{self.coordinator.data.info.mac_address}_{self._segment}"
)
+ self._attr_color_mode = COLOR_MODE_RGB
+ self._attr_supported_color_modes = {COLOR_MODE_RGB}
+ if self._rgbw and self._wv:
+ self._attr_color_mode = COLOR_MODE_RGBW
+ self._attr_supported_color_modes = {COLOR_MODE_RGBW}
+
@property
def available(self) -> bool:
"""Return True if entity is available."""
@@ -195,29 +212,31 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
- playlist = self.coordinator.data.state.playlist
+ playlist: int | None = self.coordinator.data.state.playlist
if playlist == -1:
playlist = None
- preset = self.coordinator.data.state.preset
- if preset == -1:
- preset = None
-
segment = self.coordinator.data.state.segments[self._segment]
return {
ATTR_INTENSITY: segment.intensity,
ATTR_PALETTE: segment.palette.name,
ATTR_PLAYLIST: playlist,
- ATTR_PRESET: preset,
ATTR_REVERSE: segment.reverse,
ATTR_SPEED: segment.speed,
}
@property
- def hs_color(self) -> tuple[float, float]:
- """Return the hue and saturation color value [float, float]."""
- color = self.coordinator.data.state.segments[self._segment].color_primary
- return color_util.color_RGB_to_hs(*color[:3])
+ def rgb_color(self) -> tuple[int, int, int] | None:
+ """Return the color value."""
+ return self.coordinator.data.state.segments[self._segment].color_primary[:3]
+
+ @property
+ def rgbw_color(self) -> tuple[int, int, int, int] | None:
+ """Return the color value."""
+ return cast(
+ Tuple[int, int, int, int],
+ self.coordinator.data.state.segments[self._segment].color_primary,
+ )
@property
def effect(self) -> str | None:
@@ -231,35 +250,13 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
# If this is the one and only segment, calculate brightness based
# on the master and segment brightness
- if len(state.segments) == 1:
+ if not self.coordinator.has_master_light:
return int(
(state.segments[self._segment].brightness * state.brightness) / 255
)
return state.segments[self._segment].brightness
- @property
- def white_value(self) -> int | None:
- """Return the white value of this light between 0..255."""
- color = self.coordinator.data.state.segments[self._segment].color_primary
- return color[-1] if self._rgbw else None
-
- @property
- def supported_features(self) -> int:
- """Flag supported features."""
- flags = (
- SUPPORT_BRIGHTNESS
- | SUPPORT_COLOR
- | SUPPORT_COLOR_TEMP
- | SUPPORT_EFFECT
- | SUPPORT_TRANSITION
- )
-
- if self._rgbw:
- flags |= SUPPORT_WHITE_VALUE
-
- return flags
-
@property
def effect_list(self) -> list[str]:
"""Return the list of supported effects."""
@@ -270,8 +267,9 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
"""Return the state of the light."""
state = self.coordinator.data.state
- # If there is a single segment, take master into account
- if len(state.segments) == 1 and not state.on:
+ # If there is no master, we take the master state into account
+ # on the segment level.
+ if not self.coordinator.has_master_light and not state.on:
return False
return bool(state.segments[self._segment].on)
@@ -279,19 +277,19 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
@wled_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
- data: dict[str, bool | int] = {ATTR_ON: False}
-
+ transition = None
if ATTR_TRANSITION in kwargs:
# WLED uses 100ms per unit, so 10 = 1 second.
- data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10)
+ transition = round(kwargs[ATTR_TRANSITION] * 10)
- # If there is a single segment, control via the master
- if len(self.coordinator.data.state.segments) == 1:
- await self.coordinator.wled.master(**data)
+ # If there is no master control, and only 1 segment, handle the
+ if not self.coordinator.has_master_light:
+ await self.coordinator.wled.master(on=False, transition=transition)
return
- data[ATTR_SEGMENT_ID] = self._segment
- await self.coordinator.wled.segment(**data)
+ await self.coordinator.wled.segment(
+ segment_id=self._segment, on=False, transition=transition
+ )
@wled_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
@@ -301,17 +299,11 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
ATTR_SEGMENT_ID: self._segment,
}
- if ATTR_COLOR_TEMP in kwargs:
- mireds = color_util.color_temperature_kelvin_to_mired(
- kwargs[ATTR_COLOR_TEMP]
- )
- data[ATTR_COLOR_PRIMARY] = tuple(
- map(int, color_util.color_temperature_to_rgb(mireds))
- )
+ if ATTR_RGB_COLOR in kwargs:
+ data[ATTR_COLOR_PRIMARY] = kwargs[ATTR_RGB_COLOR]
- if ATTR_HS_COLOR in kwargs:
- hue, sat = kwargs[ATTR_HS_COLOR]
- data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100)
+ if ATTR_RGBW_COLOR in kwargs:
+ data[ATTR_COLOR_PRIMARY] = kwargs[ATTR_RGBW_COLOR]
if ATTR_TRANSITION in kwargs:
# WLED uses 100ms per unit, so 10 = 1 second.
@@ -323,30 +315,8 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
if ATTR_EFFECT in kwargs:
data[ATTR_EFFECT] = kwargs[ATTR_EFFECT]
- # Support for RGBW strips, adds white value
- if self._rgbw and any(
- x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_WHITE_VALUE) for x in kwargs
- ):
- # WLED cannot just accept a white value, it needs the color.
- # We use the last know color in case just the white value changes.
- if all(x not in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs):
- hue, sat = self.hs_color
- data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100)
-
- # On a RGBW strip, when the color is pure white, disable the RGB LEDs in
- # WLED by setting RGB to 0,0,0
- if data[ATTR_COLOR_PRIMARY] == (255, 255, 255):
- data[ATTR_COLOR_PRIMARY] = (0, 0, 0)
-
- # Add requested or last known white value
- if ATTR_WHITE_VALUE in kwargs:
- data[ATTR_COLOR_PRIMARY] += (kwargs[ATTR_WHITE_VALUE],)
- else:
- data[ATTR_COLOR_PRIMARY] += (self.white_value,)
-
- # When only 1 segment is present, switch along the master, and use
- # the master for power/brightness control.
- if len(self.coordinator.data.state.segments) == 1:
+ # If there is no master control, and only 1 segment, handle the master
+ if not self.coordinator.has_master_light:
master_data = {ATTR_ON: True}
if ATTR_BRIGHTNESS in data:
master_data[ATTR_BRIGHTNESS] = data[ATTR_BRIGHTNESS]
@@ -372,24 +342,14 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
speed: int | None = None,
) -> None:
"""Set the effect of a WLED light."""
- data: dict[str, bool | int | str | None] = {ATTR_SEGMENT_ID: self._segment}
-
- if effect is not None:
- data[ATTR_EFFECT] = effect
-
- if intensity is not None:
- data[ATTR_INTENSITY] = intensity
-
- if palette is not None:
- data[ATTR_PALETTE] = palette
-
- if reverse is not None:
- data[ATTR_REVERSE] = reverse
-
- if speed is not None:
- data[ATTR_SPEED] = speed
-
- await self.coordinator.wled.segment(**data)
+ await self.coordinator.wled.segment(
+ segment_id=self._segment,
+ effect=effect,
+ intensity=intensity,
+ palette=palette,
+ reverse=reverse,
+ speed=speed,
+ )
@wled_exception_handler
async def async_preset(
@@ -397,61 +357,29 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
preset: int,
) -> None:
"""Set a WLED light to a saved preset."""
- data = {ATTR_PRESET: preset}
-
- await self.coordinator.wled.preset(**data)
+ await self.coordinator.wled.preset(preset=preset)
@callback
def async_update_segments(
- entry: ConfigEntry,
coordinator: WLEDDataUpdateCoordinator,
- current: dict[int, WLEDSegmentLight | WLEDMasterLight],
+ current_ids: set[int],
async_add_entities,
) -> None:
"""Update segments."""
segment_ids = {light.segment_id for light in coordinator.data.state.segments}
- current_ids = set(current)
+ new_entities: list[WLEDMasterLight | WLEDSegmentLight] = []
- # Discard master (if present)
- current_ids.discard(-1)
+ # More than 1 segment now? No master? Add master controls
+ if not coordinator.keep_master_light and (
+ len(current_ids) < 2 and len(segment_ids) > 1
+ ):
+ new_entities.append(WLEDMasterLight(coordinator))
# Process new segments, add them to Home Assistant
- new_entities = []
for segment_id in segment_ids - current_ids:
- current[segment_id] = WLEDSegmentLight(coordinator, segment_id)
- new_entities.append(current[segment_id])
-
- # More than 1 segment now? Add master controls
- if len(current_ids) < 2 and len(segment_ids) > 1:
- current[-1] = WLEDMasterLight(coordinator)
- new_entities.append(current[-1])
+ current_ids.add(segment_id)
+ new_entities.append(WLEDSegmentLight(coordinator, segment_id))
if new_entities:
async_add_entities(new_entities)
-
- # Process deleted segments, remove them from Home Assistant
- for segment_id in current_ids - segment_ids:
- coordinator.hass.async_create_task(
- async_remove_entity(segment_id, coordinator, current)
- )
-
- # Remove master if there is only 1 segment left
- if len(current_ids) > 1 and len(segment_ids) < 2:
- coordinator.hass.async_create_task(
- async_remove_entity(-1, coordinator, current)
- )
-
-
-async def async_remove_entity(
- index: int,
- coordinator: WLEDDataUpdateCoordinator,
- current: dict[int, WLEDSegmentLight | WLEDMasterLight],
-) -> None:
- """Remove WLED segment light from Home Assistant."""
- entity = current[index]
- await entity.async_remove(force_remove=True)
- registry = await async_get_entity_registry(coordinator.hass)
- if entity.entity_id in registry.entities:
- registry.async_remove(entity.entity_id)
- del current[index]
diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json
index b0768897076..348109f6b87 100644
--- a/homeassistant/components/wled/manifest.json
+++ b/homeassistant/components/wled/manifest.json
@@ -3,9 +3,9 @@
"name": "WLED",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wled",
- "requirements": ["wled==0.4.4"],
+ "requirements": ["wled==0.7.1"],
"zeroconf": ["_wled._tcp.local."],
"codeowners": ["@frenck"],
"quality_scale": "platinum",
- "iot_class": "local_polling"
+ "iot_class": "local_push"
}
diff --git a/homeassistant/components/wled/models.py b/homeassistant/components/wled/models.py
new file mode 100644
index 00000000000..de8628fc755
--- /dev/null
+++ b/homeassistant/components/wled/models.py
@@ -0,0 +1,30 @@
+"""Models for WLED."""
+from homeassistant.const import (
+ ATTR_IDENTIFIERS,
+ ATTR_MANUFACTURER,
+ ATTR_MODEL,
+ ATTR_NAME,
+ ATTR_SW_VERSION,
+)
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN
+from .coordinator import WLEDDataUpdateCoordinator
+
+
+class WLEDEntity(CoordinatorEntity):
+ """Defines a base WLED entity."""
+
+ coordinator: WLEDDataUpdateCoordinator
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device information about this WLED device."""
+ return {
+ ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)},
+ ATTR_NAME: self.coordinator.data.info.name,
+ ATTR_MANUFACTURER: self.coordinator.data.info.brand,
+ ATTR_MODEL: self.coordinator.data.info.product,
+ ATTR_SW_VERSION: self.coordinator.data.info.version,
+ }
diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py
new file mode 100644
index 00000000000..373565b7ef7
--- /dev/null
+++ b/homeassistant/components/wled/select.py
@@ -0,0 +1,135 @@
+"""Support for LED selects."""
+from __future__ import annotations
+
+from functools import partial
+
+from wled import Preset
+
+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 .const import DOMAIN
+from .coordinator import WLEDDataUpdateCoordinator
+from .helpers import wled_exception_handler
+from .models import WLEDEntity
+
+PARALLEL_UPDATES = 1
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up WLED select based on a config entry."""
+ coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+
+ async_add_entities([WLEDPresetSelect(coordinator)])
+
+ update_segments = partial(
+ async_update_segments,
+ coordinator,
+ set(),
+ async_add_entities,
+ )
+ coordinator.async_add_listener(update_segments)
+ update_segments()
+
+
+class WLEDPresetSelect(WLEDEntity, SelectEntity):
+ """Defined a WLED Preset select."""
+
+ _attr_icon = "mdi:playlist-play"
+
+ def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None:
+ """Initialize WLED ."""
+ super().__init__(coordinator=coordinator)
+
+ self._attr_name = f"{coordinator.data.info.name} Preset"
+ self._attr_unique_id = f"{coordinator.data.info.mac_address}_preset"
+ self._attr_options = [preset.name for preset in self.coordinator.data.presets]
+
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return len(self.coordinator.data.presets) > 0 and super().available
+
+ @property
+ def current_option(self) -> str | None:
+ """Return the current selected preset."""
+ if not isinstance(self.coordinator.data.state.preset, Preset):
+ return None
+ return self.coordinator.data.state.preset.name
+
+ @wled_exception_handler
+ async def async_select_option(self, option: str) -> None:
+ """Set WLED segment to the selected preset."""
+ await self.coordinator.wled.preset(preset=option)
+
+
+class WLEDPaletteSelect(WLEDEntity, SelectEntity):
+ """Defines a WLED Palette select."""
+
+ _attr_icon = "mdi:palette-outline"
+ _segment: int
+ _attr_entity_registry_enabled_default = False
+
+ def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None:
+ """Initialize WLED ."""
+ super().__init__(coordinator=coordinator)
+
+ # Segment 0 uses a simpler name, which is more natural for when using
+ # a single segment / using WLED with one big LED strip.
+ self._attr_name = (
+ f"{coordinator.data.info.name} Segment {segment} Color Palette"
+ )
+ if segment == 0:
+ self._attr_name = f"{coordinator.data.info.name} Color Palette"
+
+ self._attr_unique_id = f"{coordinator.data.info.mac_address}_palette_{segment}"
+ self._attr_options = [
+ palette.name for palette in self.coordinator.data.palettes
+ ]
+ self._segment = segment
+
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ try:
+ self.coordinator.data.state.segments[self._segment]
+ except IndexError:
+ return False
+
+ return super().available
+
+ @property
+ def current_option(self) -> str | None:
+ """Return the current selected color palette."""
+ return self.coordinator.data.state.segments[self._segment].palette.name
+
+ @wled_exception_handler
+ async def async_select_option(self, option: str) -> None:
+ """Set WLED segment to the selected color palette."""
+ await self.coordinator.wled.segment(segment_id=self._segment, palette=option)
+
+
+@callback
+def async_update_segments(
+ coordinator: WLEDDataUpdateCoordinator,
+ current_ids: set[int],
+ async_add_entities,
+) -> None:
+ """Update segments."""
+ segment_ids = {segment.segment_id for segment in coordinator.data.state.segments}
+
+ new_entities = []
+
+ # Process new segments, add them to Home Assistant
+ for segment_id in segment_ids - current_ids:
+ current_ids.add(segment_id)
+ new_entities.append(WLEDPaletteSelect(coordinator, segment_id))
+
+ if new_entities:
+ async_add_entities(new_entities)
diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py
index 73c012f25c7..37311e333c3 100644
--- a/homeassistant/components/wled/sensor.py
+++ b/homeassistant/components/wled/sensor.py
@@ -17,8 +17,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utcnow
-from . import WLEDDataUpdateCoordinator, WLEDEntity
from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN
+from .coordinator import WLEDDataUpdateCoordinator
+from .models import WLEDEntity
async def async_setup_entry(
@@ -39,7 +40,7 @@ async def async_setup_entry(
WLEDWifiSignalSensor(coordinator),
]
- async_add_entities(sensors, True)
+ async_add_entities(sensors)
class WLEDEstimatedCurrentSensor(WLEDEntity, SensorEntity):
@@ -121,8 +122,10 @@ class WLEDWifiSignalSensor(WLEDEntity, SensorEntity):
self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_signal"
@property
- def state(self) -> int:
+ def state(self) -> int | None:
"""Return the state of the sensor."""
+ if not self.coordinator.data.info.wifi:
+ return None
return self.coordinator.data.info.wifi.signal
@@ -140,8 +143,10 @@ class WLEDWifiRSSISensor(WLEDEntity, SensorEntity):
self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_rssi"
@property
- def state(self) -> int:
+ def state(self) -> int | None:
"""Return the state of the sensor."""
+ if not self.coordinator.data.info.wifi:
+ return None
return self.coordinator.data.info.wifi.rssi
@@ -158,8 +163,10 @@ class WLEDWifiChannelSensor(WLEDEntity, SensorEntity):
self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_channel"
@property
- def state(self) -> int:
+ def state(self) -> int | None:
"""Return the state of the sensor."""
+ if not self.coordinator.data.info.wifi:
+ return None
return self.coordinator.data.info.wifi.channel
@@ -176,6 +183,8 @@ class WLEDWifiBSSIDSensor(WLEDEntity, SensorEntity):
self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_bssid"
@property
- def state(self) -> str:
+ def state(self) -> str | None:
"""Return the state of the sensor."""
+ if not self.coordinator.data.info.wifi:
+ return None
return self.coordinator.data.info.wifi.bssid
diff --git a/homeassistant/components/wled/strings.json b/homeassistant/components/wled/strings.json
index c42a6cdffb1..9717637fdbb 100644
--- a/homeassistant/components/wled/strings.json
+++ b/homeassistant/components/wled/strings.json
@@ -20,5 +20,14 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "keep_master_light": "Keep master light, even with 1 LED segment."
+ }
+ }
+ }
}
}
diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py
index 2d1801a0c5e..b17572f7607 100644
--- a/homeassistant/components/wled/switch.py
+++ b/homeassistant/components/wled/switch.py
@@ -8,7 +8,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import WLEDDataUpdateCoordinator, WLEDEntity, wled_exception_handler
from .const import (
ATTR_DURATION,
ATTR_FADE,
@@ -16,6 +15,9 @@ from .const import (
ATTR_UDP_PORT,
DOMAIN,
)
+from .coordinator import WLEDDataUpdateCoordinator
+from .helpers import wled_exception_handler
+from .models import WLEDEntity
PARALLEL_UPDATES = 1
@@ -33,7 +35,7 @@ async def async_setup_entry(
WLEDSyncSendSwitch(coordinator),
WLEDSyncReceiveSwitch(coordinator),
]
- async_add_entities(switches, True)
+ async_add_entities(switches)
class WLEDNightlightSwitch(WLEDEntity, SwitchEntity):
diff --git a/homeassistant/components/wled/translations/ca.json b/homeassistant/components/wled/translations/ca.json
index bbc5b5232cf..2255a3cec0d 100644
--- a/homeassistant/components/wled/translations/ca.json
+++ b/homeassistant/components/wled/translations/ca.json
@@ -20,5 +20,14 @@
"title": "Dispositiu WLED descobert"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "keep_master_light": "Mant\u00e9 el llum principal, fins i tot amb 1 segment LED."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/de.json b/homeassistant/components/wled/translations/de.json
index 0dd13f763d6..d03ef92d041 100644
--- a/homeassistant/components/wled/translations/de.json
+++ b/homeassistant/components/wled/translations/de.json
@@ -7,7 +7,7 @@
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
},
- "flow_title": "WLED: {name}",
+ "flow_title": "{name}",
"step": {
"user": {
"data": {
@@ -20,5 +20,14 @@
"title": "WLED-Ger\u00e4t entdeckt"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "keep_master_light": "Master-Licht beibehalten, auch mit 1 LED-Segment."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/en.json b/homeassistant/components/wled/translations/en.json
index 8ebf6f4d91b..a114d0218ca 100644
--- a/homeassistant/components/wled/translations/en.json
+++ b/homeassistant/components/wled/translations/en.json
@@ -20,5 +20,14 @@
"title": "Discovered WLED device"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "keep_master_light": "Keep master light, even with 1 LED segment."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json
index 77c324f46a1..c1c50986b61 100644
--- a/homeassistant/components/wled/translations/es.json
+++ b/homeassistant/components/wled/translations/es.json
@@ -20,5 +20,14 @@
"title": "Dispositivo WLED detectado"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "keep_master_light": "Mantenga la luz principal, incluso con 1 segmento de LED."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/et.json b/homeassistant/components/wled/translations/et.json
index 4771c0b3af4..b3fdec52961 100644
--- a/homeassistant/components/wled/translations/et.json
+++ b/homeassistant/components/wled/translations/et.json
@@ -20,5 +20,14 @@
"title": "Leitud WLED seade"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "keep_master_light": "Kasuta p\u00f5hivalgust isegi \u00fche LED-segmendi korral."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/he.json b/homeassistant/components/wled/translations/he.json
new file mode 100644
index 00000000000..1cd249b4daa
--- /dev/null
+++ b/homeassistant/components/wled/translations/he.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/it.json b/homeassistant/components/wled/translations/it.json
index 874639638b7..efdc761960a 100644
--- a/homeassistant/components/wled/translations/it.json
+++ b/homeassistant/components/wled/translations/it.json
@@ -20,5 +20,14 @@
"title": "Dispositivo WLED rilevato"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "keep_master_light": "Mantieni la luce principale, anche con 1 segmento LED."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/nl.json b/homeassistant/components/wled/translations/nl.json
index a06d49c8902..8423f2d3f48 100644
--- a/homeassistant/components/wled/translations/nl.json
+++ b/homeassistant/components/wled/translations/nl.json
@@ -20,5 +20,14 @@
"title": "Ontdekt WLED-apparaat"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "keep_master_light": "Houd master light, zelfs met 1 LED-segment."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/no.json b/homeassistant/components/wled/translations/no.json
index 0e5df905e29..a63871613cc 100644
--- a/homeassistant/components/wled/translations/no.json
+++ b/homeassistant/components/wled/translations/no.json
@@ -20,5 +20,14 @@
"title": "Oppdaget WLED-enhet"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "keep_master_light": "Behold hovedlys, selv med 1 LED-segment."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/ru.json b/homeassistant/components/wled/translations/ru.json
index 1aefafca5f1..b43013b34e7 100644
--- a/homeassistant/components/wled/translations/ru.json
+++ b/homeassistant/components/wled/translations/ru.json
@@ -20,5 +20,14 @@
"title": "WLED"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "keep_master_light": "\u0414\u0435\u0440\u0436\u0430\u0442\u044c \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u0432\u0435\u0442 \u0434\u0430\u0436\u0435 \u0441 \u043e\u0434\u043d\u0438\u043c \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u043d\u044b\u043c \u0441\u0435\u0433\u043c\u0435\u043d\u0442\u043e\u043c."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/zh-Hant.json b/homeassistant/components/wled/translations/zh-Hant.json
index 0980bcf59aa..b8c873b90a5 100644
--- a/homeassistant/components/wled/translations/zh-Hant.json
+++ b/homeassistant/components/wled/translations/zh-Hant.json
@@ -20,5 +20,14 @@
"title": "\u81ea\u52d5\u63a2\u7d22\u5230 WLED \u88dd\u7f6e"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "keep_master_light": "\u4fdd\u7559\u4e3b\u71c8\u5149\u3001\u5373\u4fbf\u50c5\u5269 1 \u6bb5 LED\u3002"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py
index 06f3408c6a5..ab078e438c6 100644
--- a/homeassistant/components/wolflink/__init__.py
+++ b/homeassistant/components/wolflink/__init__.py
@@ -4,7 +4,7 @@ import logging
from httpx import ConnectError, ConnectTimeout
from wolf_smartset.token_auth import InvalidAuth
-from wolf_smartset.wolf_client import FetchFailed, WolfClient
+from wolf_smartset.wolf_client import FetchFailed, ParameterReadError, WolfClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
@@ -26,13 +26,14 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor"]
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Wolf SmartSet Service from a config entry."""
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
device_name = entry.data[DEVICE_NAME]
device_id = entry.data[DEVICE_ID]
gateway_id = entry.data[DEVICE_GATEWAY]
+ refetch_parameters = False
_LOGGER.debug(
"Setting up wolflink integration for device: %s (ID: %s, gateway: %s)",
device_name,
@@ -42,17 +43,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
wolf_client = WolfClient(username, password)
- try:
- parameters = await fetch_parameters(wolf_client, gateway_id, device_id)
- except InvalidAuth:
- _LOGGER.debug("Authentication failed")
- return False
+ parameters = await fetch_parameters_init(wolf_client, gateway_id, device_id)
async def async_update_data():
"""Update all stored entities for Wolf SmartSet."""
try:
- values = await wolf_client.fetch_value(gateway_id, device_id, parameters)
- return {v.value_id: v.value for v in values}
+ nonlocal refetch_parameters
+ nonlocal parameters
+ await wolf_client.update_session()
+ if not wolf_client.fetch_system_state_list(device_id, gateway_id):
+ refetch_parameters = True
+ raise UpdateFailed(
+ "Could not fetch values from server because device is Offline."
+ )
+ if refetch_parameters:
+ parameters = await fetch_parameters(wolf_client, gateway_id, device_id)
+ hass.data[DOMAIN][entry.entry_id][PARAMETERS] = parameters
+ refetch_parameters = False
+ values = {
+ v.value_id: v.value
+ for v in await wolf_client.fetch_value(
+ gateway_id, device_id, parameters
+ )
+ }
+ return {
+ parameter.parameter_id: (
+ parameter.value_id,
+ values[parameter.value_id],
+ )
+ for parameter in parameters
+ if parameter.value_id in values
+ }
except ConnectError as exception:
raise UpdateFailed(
f"Error communicating with API: {exception}"
@@ -61,13 +82,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
raise UpdateFailed(
f"Could not fetch values from server due to: {exception}"
) from exception
+ except ParameterReadError as exception:
+ refetch_parameters = True
+ raise UpdateFailed(
+ "Could not fetch values for parameter. Refreshing value IDs."
+ ) from exception
except InvalidAuth as exception:
raise UpdateFailed("Invalid authentication during update.") from exception
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- name="wolflink",
+ name=DOMAIN,
update_method=async_update_data,
update_interval=timedelta(minutes=1),
)
@@ -100,9 +126,14 @@ async def fetch_parameters(client: WolfClient, gateway_id: int, device_id: int):
By default Reglertyp entity is removed because API will not provide value for this parameter.
"""
+ fetched_parameters = await client.fetch_parameters(gateway_id, device_id)
+ return [param for param in fetched_parameters if param.name != "Reglertyp"]
+
+
+async def fetch_parameters_init(client: WolfClient, gateway_id: int, device_id: int):
+ """Fetch all available parameters with usage of WolfClient but handles all exceptions and results in ConfigEntryNotReady."""
try:
- fetched_parameters = await client.fetch_parameters(gateway_id, device_id)
- return [param for param in fetched_parameters if param.name != "Reglertyp"]
+ return await fetch_parameters(client, gateway_id, device_id)
except (ConnectError, ConnectTimeout, FetchFailed) as exception:
raise ConfigEntryNotReady(
f"Error communicating with API: {exception}"
diff --git a/homeassistant/components/wolflink/manifest.json b/homeassistant/components/wolflink/manifest.json
index 504419ef0f4..749f7bbc67c 100644
--- a/homeassistant/components/wolflink/manifest.json
+++ b/homeassistant/components/wolflink/manifest.json
@@ -3,7 +3,7 @@
"name": "Wolf SmartSet Service",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wolflink",
- "requirements": ["wolf_smartset==0.1.8"],
+ "requirements": ["wolf_smartset==0.1.11"],
"codeowners": ["@adamkrol93"],
"iot_class": "cloud_polling"
}
diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py
index f243160ff59..0d35d4bce5c 100644
--- a/homeassistant/components/wolflink/sensor.py
+++ b/homeassistant/components/wolflink/sensor.py
@@ -65,8 +65,10 @@ class WolfLinkSensor(CoordinatorEntity, SensorEntity):
@property
def state(self):
"""Return the state. Wolf Client is returning only changed values so we need to store old value here."""
- if self.wolf_object.value_id in self.coordinator.data:
- self._state = self.coordinator.data[self.wolf_object.value_id]
+ if self.wolf_object.parameter_id in self.coordinator.data:
+ new_state = self.coordinator.data[self.wolf_object.parameter_id]
+ self.wolf_object.value_id = new_state[0]
+ self._state = new_state[1]
return self._state
@property
diff --git a/homeassistant/components/wolflink/translations/he.json b/homeassistant/components/wolflink/translations/he.json
new file mode 100644
index 00000000000..c479d8488f2
--- /dev/null
+++ b/homeassistant/components/wolflink/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"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/sensor.de.json b/homeassistant/components/wolflink/translations/sensor.de.json
index a83fb9856ad..6b1baf8b0bf 100644
--- a/homeassistant/components/wolflink/translations/sensor.de.json
+++ b/homeassistant/components/wolflink/translations/sensor.de.json
@@ -11,11 +11,15 @@
"at_frostschutz": "AT Frostschutz",
"aus": "Aus",
"auto": "",
+ "auto_off_cool": "AutoOffCool",
+ "auto_on_cool": "AutoOnCool",
"automatik_aus": "Automatik AUS",
"automatik_ein": "Automatik EIN",
+ "bereit_keine_ladung": "Bereit, keine Ladung",
"betrieb_ohne_brenner": "Betrieb ohne Brenner",
"cooling": "K\u00fchlung",
"deaktiviert": "Deaktiviert",
+ "dhw_prior": "DHWPrior",
"eco": "Eco",
"ein": "Ein",
"estrichtrocknung": "Estrichtrocknung",
@@ -25,6 +29,7 @@
"frost_warmwasser": "Warmwasser Frost",
"frostschutz": "Frostschutz",
"gasdruck": "Gasdruck",
+ "glt_betrieb": "BMS-Modus",
"gradienten_uberwachung": "Gradienten\u00fcberwachung",
"heizbetrieb": "Heizbetrieb",
"heizgerat_mit_speicher": "Heizger\u00e4t mit Speicher",
@@ -44,6 +49,7 @@
"nur_heizgerat": "Nur Heizger\u00e4t",
"parallelbetrieb": "Parallelbetrieb",
"partymodus": "Party-Modus",
+ "perm_cooling": "PermCooling",
"permanent": "Permanent",
"permanentbetrieb": "Permanentbetrieb",
"reduzierter_betrieb": "Reduzierter Betrieb",
@@ -53,9 +59,11 @@
"schornsteinfeger": "Emissionspr\u00fcfung",
"smart_grid": "SmartGrid",
"smart_home": "SmartHome",
+ "softstart": "Soft Start",
"solarbetrieb": "Solarmodus",
"sparbetrieb": "Sparmodus",
"sparen": "Sparen",
+ "spreizung_hoch": "Spreizung zu hoch",
"spreizung_kf": "Spreizung KF",
"stabilisierung": "Stabilisierung",
"standby": "Standby",
diff --git a/homeassistant/components/wolflink/translations/sensor.he.json b/homeassistant/components/wolflink/translations/sensor.he.json
new file mode 100644
index 00000000000..68b635ba82b
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/sensor.he.json
@@ -0,0 +1,11 @@
+{
+ "state": {
+ "wolflink__state": {
+ "solarbetrieb": "\u05de\u05e6\u05d1 \u05e1\u05d5\u05dc\u05d0\u05e8\u05d9",
+ "standby": "\u05de\u05e6\u05d1 \u05d4\u05de\u05ea\u05e0\u05d4",
+ "start": "\u05d4\u05ea\u05d7\u05dc",
+ "warmwasser": "DHW",
+ "zunden": "\u05d4\u05e6\u05ea\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py
index 9e4d957d028..8b45326cdbd 100644
--- a/homeassistant/components/wsdot/sensor.py
+++ b/homeassistant/components/wsdot/sensor.py
@@ -73,6 +73,8 @@ class WashingtonStateTransportSensor(SensorEntity):
can read them and make them available.
"""
+ _attr_icon = ICON
+
def __init__(self, name, access_code):
"""Initialize the sensor."""
self._data = {}
@@ -90,15 +92,12 @@ class WashingtonStateTransportSensor(SensorEntity):
"""Return the state of the sensor."""
return self._state
- @property
- def icon(self):
- """Icon to use in the frontend, if any."""
- return ICON
-
class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor):
"""Travel time sensor from WSDOT."""
+ _attr_unit_of_measurement = TIME_MINUTES
+
def __init__(self, name, access_code, travel_time_id):
"""Construct a travel time sensor."""
self._travel_time_id = travel_time_id
@@ -135,11 +134,6 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor):
)
return attrs
- @property
- def unit_of_measurement(self):
- """Return the unit this state is expressed in."""
- return TIME_MINUTES
-
def _parse_wsdot_timestamp(timestamp):
"""Convert WSDOT timestamp to datetime."""
diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py
index 58ce7587070..13cd4217b4d 100644
--- a/homeassistant/components/xbee/__init__.py
+++ b/homeassistant/components/xbee/__init__.py
@@ -369,6 +369,8 @@ class XBeeDigitalOut(XBeeDigitalIn):
class XBeeAnalogIn(SensorEntity):
"""Representation of a GPIO pin configured as an analog input."""
+ _attr_unit_of_measurement = PERCENTAGE
+
def __init__(self, config, device):
"""Initialize the XBee analog in device."""
self._config = config
@@ -418,11 +420,6 @@ class XBeeAnalogIn(SensorEntity):
"""Return the state of the entity."""
return self._value
- @property
- def unit_of_measurement(self):
- """Return the unit this state is expressed in."""
- return PERCENTAGE
-
def update(self):
"""Get the latest reading from the ADC."""
try:
diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py
index 78cfe964277..18e4b0c7aa1 100644
--- a/homeassistant/components/xbee/sensor.py
+++ b/homeassistant/components/xbee/sensor.py
@@ -46,6 +46,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class XBeeTemperatureSensor(SensorEntity):
"""Representation of XBee Pro temperature sensor."""
+ _attr_unit_of_measurement = TEMP_CELSIUS
+
def __init__(self, config, device):
"""Initialize the sensor."""
self._config = config
@@ -62,11 +64,6 @@ class XBeeTemperatureSensor(SensorEntity):
"""Return the state of the sensor."""
return self._temp
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement the value is expressed in."""
- return TEMP_CELSIUS
-
def update(self):
"""Get the latest data."""
try:
diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py
index db278d0da43..6e651cdbcf3 100644
--- a/homeassistant/components/xbox/__init__.py
+++ b/homeassistant/components/xbox/__init__.py
@@ -72,7 +72,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up xbox from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
diff --git a/homeassistant/components/xbox/binary_sensor.py b/homeassistant/components/xbox/binary_sensor.py
index 32a3126de1e..4965e9705d1 100644
--- a/homeassistant/components/xbox/binary_sensor.py
+++ b/homeassistant/components/xbox/binary_sensor.py
@@ -4,6 +4,7 @@ from __future__ import annotations
from functools import partial
from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_registry import (
async_get_registry as async_get_entity_registry,
@@ -16,16 +17,18 @@ from .const import DOMAIN
PRESENCE_ATTRIBUTES = ["online", "in_party", "in_game", "in_multiplayer"]
-async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities
+):
"""Set up Xbox Live friends."""
- coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id][
+ coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
"coordinator"
]
update_friends = partial(async_update_friends, coordinator, {}, async_add_entities)
unsub = coordinator.async_add_listener(update_friends)
- hass.data[DOMAIN][config_entry.entry_id]["binary_sensor_unsub"] = unsub
+ hass.data[DOMAIN][entry.entry_id]["binary_sensor_unsub"] = unsub
update_friends()
diff --git a/homeassistant/components/xbox/translations/he.json b/homeassistant/components/xbox/translations/he.json
new file mode 100644
index 00000000000..61069036ec5
--- /dev/null
+++ b/homeassistant/components/xbox/translations/he.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.",
+ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+ "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."
+ },
+ "create_entry": {
+ "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json
index e72f00d5f5f..87120f09605 100644
--- a/homeassistant/components/xiaomi_aqara/translations/de.json
+++ b/homeassistant/components/xiaomi_aqara/translations/de.json
@@ -12,7 +12,7 @@
"invalid_key": "Ung\u00fcltiger Gateway-Schl\u00fcssel",
"invalid_mac": "Ung\u00fcltige MAC-Adresse"
},
- "flow_title": "Xiaomi Aqara Gateway: {name}",
+ "flow_title": "{name}",
"step": {
"select": {
"data": {
diff --git a/homeassistant/components/xiaomi_aqara/translations/he.json b/homeassistant/components/xiaomi_aqara/translations/he.json
new file mode 100644
index 00000000000..5a12ddc3b9e
--- /dev/null
+++ b/homeassistant/components/xiaomi_aqara/translations/he.json
@@ -0,0 +1,24 @@
+{
+ "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"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "select": {
+ "data": {
+ "select_ip": "\u05db\u05ea\u05d5\u05d1\u05ea IP"
+ }
+ },
+ "settings": {
+ "description": "\u05e0\u05d9\u05ea\u05df \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05de\u05e4\u05ea\u05d7 (\u05e1\u05d9\u05e1\u05de\u05d4) \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d4\u05d3\u05e8\u05db\u05d4 \u05d6\u05d5: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \u05d0\u05dd \u05d4\u05de\u05e4\u05ea\u05d7 \u05d0\u05d9\u05e0\u05d5 \u05de\u05e1\u05d5\u05e4\u05e7, \u05e8\u05e7 \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d9\u05d4\u05d9\u05d5 \u05e0\u05d2\u05d9\u05e9\u05d9\u05dd"
+ },
+ "user": {
+ "data": {
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_aqara/translations/hu.json b/homeassistant/components/xiaomi_aqara/translations/hu.json
index 295fcef83fe..675ef24af3b 100644
--- a/homeassistant/components/xiaomi_aqara/translations/hu.json
+++ b/homeassistant/components/xiaomi_aqara/translations/hu.json
@@ -12,7 +12,7 @@
"invalid_key": "\u00c9rv\u00e9nytelen kulcs",
"invalid_mac": "\u00c9rv\u00e9nytelen Mac-c\u00edm"
},
- "flow_title": "Xiaomi Aqara K\u00f6zponti egys\u00e9g: {name}",
+ "flow_title": "{name}",
"step": {
"select": {
"data": {
diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py
index fa2dfcb9944..076aed4d30c 100644
--- a/homeassistant/components/xiaomi_miio/__init__.py
+++ b/homeassistant/components/xiaomi_miio/__init__.py
@@ -51,6 +51,30 @@ async def async_setup_entry(
)
+def get_platforms(config_entry):
+ """Return the platforms belonging to a config_entry."""
+ model = config_entry.data[CONF_MODEL]
+ flow_type = config_entry.data[CONF_FLOW_TYPE]
+
+ if flow_type == CONF_GATEWAY:
+ return GATEWAY_PLATFORMS
+ if flow_type == CONF_DEVICE:
+ if model in MODELS_SWITCH:
+ return SWITCH_PLATFORMS
+ if model in MODELS_FAN:
+ return FAN_PLATFORMS
+ if model in MODELS_LIGHT:
+ return LIGHT_PLATFORMS
+ for vacuum_model in MODELS_VACUUM:
+ if model.startswith(vacuum_model):
+ return VACUUM_PLATFORMS
+ for air_monitor_model in MODELS_AIR_MONITOR:
+ if model.startswith(air_monitor_model):
+ return AIR_MONITOR_PLATFORMS
+
+ return []
+
+
async def async_setup_gateway_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
@@ -64,8 +88,10 @@ async def async_setup_gateway_entry(
if entry.unique_id.endswith("-gateway"):
hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"])
+ entry.async_on_unload(entry.add_update_listener(update_listener))
+
# Connect to gateway
- gateway = ConnectXiaomiGateway(hass)
+ gateway = ConnectXiaomiGateway(hass, entry)
if not await gateway.async_connect_gateway(host, token):
return False
gateway_info = gateway.gateway_info
@@ -128,29 +154,36 @@ async def async_setup_device_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
"""Set up the Xiaomi Miio device component from a config entry."""
- model = entry.data[CONF_MODEL]
-
- # Identify platforms to setup
- platforms = []
- if model in MODELS_SWITCH:
- platforms = SWITCH_PLATFORMS
- elif model in MODELS_FAN:
- platforms = FAN_PLATFORMS
- elif model in MODELS_LIGHT:
- platforms = LIGHT_PLATFORMS
- for vacuum_model in MODELS_VACUUM:
- if model.startswith(vacuum_model):
- platforms = VACUUM_PLATFORMS
- for air_monitor_model in MODELS_AIR_MONITOR:
- if model.startswith(air_monitor_model):
- platforms = AIR_MONITOR_PLATFORMS
+ platforms = get_platforms(entry)
if not platforms:
return False
- for platform in platforms:
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, platform)
- )
+ entry.async_on_unload(entry.add_update_listener(update_listener))
+
+ hass.config_entries.async_setup_platforms(entry, platforms)
return True
+
+
+async def async_unload_entry(
+ hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
+):
+ """Unload a config entry."""
+ platforms = get_platforms(config_entry)
+
+ unload_ok = await hass.config_entries.async_unload_platforms(
+ config_entry, platforms
+ )
+
+ if unload_ok:
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+
+ return unload_ok
+
+
+async def update_listener(
+ hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
+):
+ """Handle options update."""
+ await hass.config_entries.async_reload(config_entry.entry_id)
diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py
index 59eee0e6e04..790a82a0411 100644
--- a/homeassistant/components/xiaomi_miio/config_flow.py
+++ b/homeassistant/components/xiaomi_miio/config_flow.py
@@ -2,34 +2,98 @@
import logging
from re import search
+from micloud import MiCloud
import voluptuous as vol
from homeassistant import config_entries
+from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
+from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac
from .const import (
+ CONF_CLOUD_COUNTRY,
+ CONF_CLOUD_PASSWORD,
+ CONF_CLOUD_SUBDEVICES,
+ CONF_CLOUD_USERNAME,
CONF_DEVICE,
CONF_FLOW_TYPE,
CONF_GATEWAY,
CONF_MAC,
+ CONF_MANUAL,
CONF_MODEL,
+ DEFAULT_CLOUD_COUNTRY,
DOMAIN,
MODELS_ALL,
MODELS_ALL_DEVICES,
MODELS_GATEWAY,
+ SERVER_COUNTRY_CODES,
)
from .device import ConnectXiaomiDevice
_LOGGER = logging.getLogger(__name__)
-DEFAULT_GATEWAY_NAME = "Xiaomi Gateway"
-
DEVICE_SETTINGS = {
vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
}
DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS)
-DEVICE_MODEL_CONFIG = {vol.Optional(CONF_MODEL): vol.In(MODELS_ALL)}
+DEVICE_MODEL_CONFIG = vol.Schema({vol.Required(CONF_MODEL): vol.In(MODELS_ALL)})
+DEVICE_CLOUD_CONFIG = vol.Schema(
+ {
+ vol.Optional(CONF_CLOUD_USERNAME): str,
+ vol.Optional(CONF_CLOUD_PASSWORD): str,
+ vol.Optional(CONF_CLOUD_COUNTRY, default=DEFAULT_CLOUD_COUNTRY): vol.In(
+ SERVER_COUNTRY_CODES
+ ),
+ vol.Optional(CONF_MANUAL, default=False): bool,
+ }
+)
+
+
+class OptionsFlowHandler(config_entries.OptionsFlow):
+ """Options for the component."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
+ """Init object."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Manage the options."""
+ errors = {}
+ if user_input is not None:
+ use_cloud = user_input.get(CONF_CLOUD_SUBDEVICES, False)
+ cloud_username = self.config_entry.data.get(CONF_CLOUD_USERNAME)
+ cloud_password = self.config_entry.data.get(CONF_CLOUD_PASSWORD)
+ cloud_country = self.config_entry.data.get(CONF_CLOUD_COUNTRY)
+
+ if use_cloud and (
+ not cloud_username or not cloud_password or not cloud_country
+ ):
+ errors["base"] = "cloud_credentials_incomplete"
+ # trigger re-auth flow
+ self.hass.async_create_task(
+ self.hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_REAUTH},
+ data=self.config_entry.data,
+ )
+ )
+
+ if not errors:
+ return self.async_create_entry(title="", data=user_input)
+
+ settings_schema = vol.Schema(
+ {
+ vol.Optional(
+ CONF_CLOUD_SUBDEVICES,
+ default=self.config_entry.options.get(CONF_CLOUD_SUBDEVICES, False),
+ ): bool
+ }
+ )
+
+ return self.async_show_form(
+ step_id="init", data_schema=settings_schema, errors=errors
+ )
class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@@ -41,16 +105,51 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Initialize."""
self.host = None
self.mac = None
+ self.token = None
+ self.model = None
+ self.name = None
+ self.cloud_username = None
+ self.cloud_password = None
+ self.cloud_country = None
+ self.cloud_devices = {}
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry) -> OptionsFlowHandler:
+ """Get the options flow."""
+ return OptionsFlowHandler(config_entry)
+
+ async def async_step_reauth(self, user_input=None):
+ """Perform reauth upon an authentication error or missing cloud credentials."""
+ self.host = user_input[CONF_HOST]
+ self.token = user_input[CONF_TOKEN]
+ self.mac = user_input[CONF_MAC]
+ self.model = user_input.get(CONF_MODEL)
+ return await self.async_step_reauth_confirm()
+
+ async def async_step_reauth_confirm(self, user_input=None):
+ """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({})
+ )
async def async_step_import(self, conf: dict):
"""Import a configuration from config.yaml."""
- host = conf[CONF_HOST]
- self.context.update({"title_placeholders": {"name": f"YAML import {host}"}})
- return await self.async_step_device(user_input=conf)
+ self.host = conf[CONF_HOST]
+ self.token = conf[CONF_TOKEN]
+ self.name = conf.get(CONF_NAME)
+ self.model = conf.get(CONF_MODEL)
+
+ self.context.update(
+ {"title_placeholders": {"name": f"YAML import {self.host}"}}
+ )
+ return await self.async_step_connect()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
- return await self.async_step_device()
+ return await self.async_step_cloud()
async def async_step_zeroconf(self, discovery_info):
"""Handle zeroconf discovery."""
@@ -79,7 +178,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
{"title_placeholders": {"name": f"Gateway {self.host}"}}
)
- return await self.async_step_device()
+ return await self.async_step_cloud()
for device_model in MODELS_ALL_DEVICES:
if name.startswith(device_model.replace(".", "-")):
@@ -91,7 +190,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
{"title_placeholders": {"name": f"{device_model} {self.host}"}}
)
- return await self.async_step_device()
+ return await self.async_step_cloud()
# Discovered device is not yet supported
_LOGGER.debug(
@@ -101,76 +200,190 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
)
return self.async_abort(reason="not_xiaomi_miio")
- async def async_step_device(self, user_input=None):
- """Handle a flow initialized by the user to configure a xiaomi miio device."""
+ def extract_cloud_info(self, cloud_device_info):
+ """Extract the cloud info."""
+ if self.host is None:
+ self.host = cloud_device_info["localip"]
+ if self.mac is None:
+ self.mac = format_mac(cloud_device_info["mac"])
+ if self.model is None:
+ self.model = cloud_device_info["model"]
+ if self.name is None:
+ self.name = cloud_device_info["name"]
+ self.token = cloud_device_info["token"]
+
+ async def async_step_cloud(self, user_input=None):
+ """Configure a xiaomi miio device through the Miio Cloud."""
errors = {}
if user_input is not None:
- token = user_input[CONF_TOKEN]
- model = user_input.get(CONF_MODEL)
+ if user_input[CONF_MANUAL]:
+ return await self.async_step_manual()
+
+ cloud_username = user_input.get(CONF_CLOUD_USERNAME)
+ cloud_password = user_input.get(CONF_CLOUD_PASSWORD)
+ cloud_country = user_input.get(CONF_CLOUD_COUNTRY)
+
+ if not cloud_username or not cloud_password or not cloud_country:
+ errors["base"] = "cloud_credentials_incomplete"
+ return self.async_show_form(
+ step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
+ )
+
+ miio_cloud = MiCloud(cloud_username, cloud_password)
+ if not await self.hass.async_add_executor_job(miio_cloud.login):
+ errors["base"] = "cloud_login_error"
+ return self.async_show_form(
+ step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
+ )
+
+ devices_raw = await self.hass.async_add_executor_job(
+ miio_cloud.get_devices, cloud_country
+ )
+
+ if not devices_raw:
+ errors["base"] = "cloud_no_devices"
+ return self.async_show_form(
+ step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
+ )
+
+ self.cloud_devices = {}
+ for device in devices_raw:
+ parent_id = device.get("parent_id")
+ if not parent_id:
+ name = device["name"]
+ model = device["model"]
+ list_name = f"{name} - {model}"
+ self.cloud_devices[list_name] = device
+
+ self.cloud_username = cloud_username
+ self.cloud_password = cloud_password
+ self.cloud_country = cloud_country
+
+ if self.host is not None:
+ for device in self.cloud_devices.values():
+ cloud_host = device.get("localip")
+ if cloud_host == self.host:
+ self.extract_cloud_info(device)
+ return await self.async_step_connect()
+
+ if len(self.cloud_devices) == 1:
+ self.extract_cloud_info(list(self.cloud_devices.values())[0])
+ return await self.async_step_connect()
+
+ return await self.async_step_select()
+
+ return self.async_show_form(
+ step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
+ )
+
+ async def async_step_select(self, user_input=None):
+ """Handle multiple cloud devices found."""
+ errors = {}
+ if user_input is not None:
+ cloud_device = self.cloud_devices[user_input["select_device"]]
+ self.extract_cloud_info(cloud_device)
+ return await self.async_step_connect()
+
+ select_schema = vol.Schema(
+ {vol.Required("select_device"): vol.In(list(self.cloud_devices))}
+ )
+
+ return self.async_show_form(
+ step_id="select", data_schema=select_schema, errors=errors
+ )
+
+ async def async_step_manual(self, user_input=None):
+ """Configure a xiaomi miio device Manually."""
+ errors = {}
+ if user_input is not None:
+ self.token = user_input[CONF_TOKEN]
if user_input.get(CONF_HOST):
self.host = user_input[CONF_HOST]
- # Try to connect to a Xiaomi Device.
- connect_device_class = ConnectXiaomiDevice(self.hass)
- await connect_device_class.async_connect_device(self.host, token)
- device_info = connect_device_class.device_info
-
- if model is None and device_info is not None:
- model = device_info.model
-
- if model is not None:
- if self.mac is None and device_info is not None:
- self.mac = format_mac(device_info.mac_address)
-
- # Setup Gateways
- for gateway_model in MODELS_GATEWAY:
- if model.startswith(gateway_model):
- unique_id = self.mac
- await self.async_set_unique_id(
- unique_id, raise_on_progress=False
- )
- self._abort_if_unique_id_configured()
- return self.async_create_entry(
- title=DEFAULT_GATEWAY_NAME,
- data={
- CONF_FLOW_TYPE: CONF_GATEWAY,
- CONF_HOST: self.host,
- CONF_TOKEN: token,
- CONF_MODEL: model,
- CONF_MAC: self.mac,
- },
- )
-
- # Setup all other Miio Devices
- name = user_input.get(CONF_NAME, model)
-
- for device_model in MODELS_ALL_DEVICES:
- if model.startswith(device_model):
- unique_id = self.mac
- await self.async_set_unique_id(
- unique_id, raise_on_progress=False
- )
- self._abort_if_unique_id_configured()
- return self.async_create_entry(
- title=name,
- data={
- CONF_FLOW_TYPE: CONF_DEVICE,
- CONF_HOST: self.host,
- CONF_TOKEN: token,
- CONF_MODEL: model,
- CONF_MAC: self.mac,
- },
- )
- errors["base"] = "unknown_device"
- else:
- errors["base"] = "cannot_connect"
+ return await self.async_step_connect()
if self.host:
schema = vol.Schema(DEVICE_SETTINGS)
else:
schema = DEVICE_CONFIG
- if errors:
- schema = schema.extend(DEVICE_MODEL_CONFIG)
+ return self.async_show_form(step_id="manual", data_schema=schema, errors=errors)
- return self.async_show_form(step_id="device", data_schema=schema, errors=errors)
+ async def async_step_connect(self, user_input=None):
+ """Connect to a xiaomi miio device."""
+ errors = {}
+ if self.host is None or self.token is None:
+ return self.async_abort(reason="incomplete_info")
+
+ if user_input is not None:
+ self.model = user_input[CONF_MODEL]
+
+ # Try to connect to a Xiaomi Device.
+ connect_device_class = ConnectXiaomiDevice(self.hass)
+ await connect_device_class.async_connect_device(self.host, self.token)
+ device_info = connect_device_class.device_info
+
+ if self.model is None and device_info is not None:
+ self.model = device_info.model
+
+ if self.model is None:
+ errors["base"] = "cannot_connect"
+ return self.async_show_form(
+ step_id="connect", data_schema=DEVICE_MODEL_CONFIG, errors=errors
+ )
+
+ if self.mac is None and device_info is not None:
+ self.mac = format_mac(device_info.mac_address)
+
+ unique_id = self.mac
+ existing_entry = await self.async_set_unique_id(
+ unique_id, raise_on_progress=False
+ )
+ if existing_entry:
+ data = existing_entry.data.copy()
+ data[CONF_HOST] = self.host
+ data[CONF_TOKEN] = self.token
+ if (
+ self.cloud_username is not None
+ and self.cloud_password is not None
+ and self.cloud_country is not None
+ ):
+ data[CONF_CLOUD_USERNAME] = self.cloud_username
+ data[CONF_CLOUD_PASSWORD] = self.cloud_password
+ data[CONF_CLOUD_COUNTRY] = self.cloud_country
+ self.hass.config_entries.async_update_entry(existing_entry, data=data)
+ await self.hass.config_entries.async_reload(existing_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+
+ if self.name is None:
+ self.name = self.model
+
+ flow_type = None
+ for gateway_model in MODELS_GATEWAY:
+ if self.model.startswith(gateway_model):
+ flow_type = CONF_GATEWAY
+
+ if flow_type is None:
+ for device_model in MODELS_ALL_DEVICES:
+ if self.model.startswith(device_model):
+ flow_type = CONF_DEVICE
+
+ if flow_type is not None:
+ return self.async_create_entry(
+ title=self.name,
+ data={
+ CONF_FLOW_TYPE: flow_type,
+ CONF_HOST: self.host,
+ CONF_TOKEN: self.token,
+ CONF_MODEL: self.model,
+ CONF_MAC: self.mac,
+ CONF_CLOUD_USERNAME: self.cloud_username,
+ CONF_CLOUD_PASSWORD: self.cloud_password,
+ CONF_CLOUD_COUNTRY: self.cloud_country,
+ },
+ )
+
+ errors["base"] = "unknown_device"
+ return self.async_show_form(
+ step_id="connect", data_schema=DEVICE_MODEL_CONFIG, errors=errors
+ )
diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py
index 35c4d4a1662..27d0a34bf39 100644
--- a/homeassistant/components/xiaomi_miio/const.py
+++ b/homeassistant/components/xiaomi_miio/const.py
@@ -1,16 +1,28 @@
"""Constants for the Xiaomi Miio component."""
DOMAIN = "xiaomi_miio"
+# Config flow
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"
+CONF_CLOUD_COUNTRY = "cloud_country"
+CONF_MANUAL = "manual"
+
+# Options flow
+CONF_CLOUD_SUBDEVICES = "cloud_subdevices"
KEY_COORDINATOR = "coordinator"
ATTR_AVAILABLE = "available"
+# Cloud
+SERVER_COUNTRY_CODES = ["cn", "de", "i2", "ru", "sg", "us"]
+DEFAULT_CLOUD_COUNTRY = "cn"
+
# Fan Models
MODEL_AIRPURIFIER_V1 = "zhimi.airpurifier.v1"
MODEL_AIRPURIFIER_V2 = "zhimi.airpurifier.v2"
diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py
index cb91726ecad..081b910efdb 100644
--- a/homeassistant/components/xiaomi_miio/device.py
+++ b/homeassistant/components/xiaomi_miio/device.py
@@ -1,8 +1,10 @@
"""Code to handle a Xiaomi Device."""
import logging
+from construct.core import ChecksumError
from miio import Device, DeviceException
+from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import Entity
@@ -33,17 +35,24 @@ class ConnectXiaomiDevice:
async def async_connect_device(self, host, token):
"""Connect to the Xiaomi Device."""
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
+
try:
self._device = Device(host, token)
# get the device info
self._device_info = await self._hass.async_add_executor_job(
self._device.info
)
- except DeviceException:
+ except DeviceException as error:
+ if isinstance(error.__cause__, ChecksumError):
+ raise ConfigEntryAuthFailed(error) from error
+
_LOGGER.error(
- "DeviceException during setup of xiaomi device with host %s", host
+ "DeviceException during setup of xiaomi device with host %s: %s",
+ host,
+ error,
)
return False
+
_LOGGER.debug(
"%s %s %s detected",
self._device_info.model,
diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py
index a485654e638..a5a5122ea07 100644
--- a/homeassistant/components/xiaomi_miio/fan.py
+++ b/homeassistant/components/xiaomi_miio/fan.py
@@ -3,6 +3,7 @@ import asyncio
from enum import Enum
from functools import partial
import logging
+import math
from miio import (
AirFresh,
@@ -40,6 +41,7 @@ from homeassistant.components.fan import (
SPEED_HIGH,
SPEED_LOW,
SPEED_MEDIUM,
+ SUPPORT_PRESET_MODE,
SUPPORT_SET_SPEED,
FanEntity,
)
@@ -53,6 +55,10 @@ from homeassistant.const import (
CONF_TOKEN,
)
import homeassistant.helpers.config_validation as cv
+from homeassistant.util.percentage import (
+ percentage_to_ranged_value,
+ ranged_value_to_percentage,
+)
from .const import (
CONF_DEVICE,
@@ -157,7 +163,6 @@ ATTR_DRY = "dry"
# Air Humidifier CA4
ATTR_ACTUAL_MOTOR_SPEED = "actual_speed"
ATTR_FAHRENHEIT = "fahrenheit"
-ATTR_FAULT = "fault"
# Air Fresh
ATTR_CO2 = "co2"
@@ -332,10 +337,15 @@ AVAILABLE_ATTRIBUTES_AIRFRESH = {
}
OPERATION_MODES_AIRPURIFIER = ["Auto", "Silent", "Favorite", "Idle"]
+PRESET_MODES_AIRPURIFIER = ["Auto", "Silent", "Favorite", "Idle"]
OPERATION_MODES_AIRPURIFIER_PRO = ["Auto", "Silent", "Favorite"]
+PRESET_MODES_AIRPURIFIER_PRO = ["Auto", "Silent", "Favorite"]
OPERATION_MODES_AIRPURIFIER_PRO_V7 = OPERATION_MODES_AIRPURIFIER_PRO
+PRESET_MODES_AIRPURIFIER_PRO_V7 = PRESET_MODES_AIRPURIFIER_PRO
OPERATION_MODES_AIRPURIFIER_2S = ["Auto", "Silent", "Favorite"]
+PRESET_MODES_AIRPURIFIER_2S = ["Auto", "Silent", "Favorite"]
OPERATION_MODES_AIRPURIFIER_3 = ["Auto", "Silent", "Favorite", "Fan"]
+PRESET_MODES_AIRPURIFIER_3 = ["Auto", "Silent", "Favorite", "Fan"]
OPERATION_MODES_AIRPURIFIER_V3 = [
"Auto",
"Silent",
@@ -345,7 +355,19 @@ OPERATION_MODES_AIRPURIFIER_V3 = [
"High",
"Strong",
]
+PRESET_MODES_AIRPURIFIER_V3 = [
+ "Auto",
+ "Silent",
+ "Favorite",
+ "Idle",
+ "Medium",
+ "High",
+ "Strong",
+]
OPERATION_MODES_AIRFRESH = ["Auto", "Silent", "Interval", "Low", "Middle", "Strong"]
+PRESET_MODES_AIRFRESH = ["Auto", "Interval"]
+PRESET_MODES_AIRHUMIDIFIER = ["Auto"]
+PRESET_MODES_AIRHUMIDIFIER_CA4 = ["Auto"]
SUCCESS = ["ok"]
@@ -635,11 +657,42 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity):
self._state_attrs = {ATTR_MODEL: self._model}
self._device_features = FEATURE_SET_CHILD_LOCK
self._skip_update = False
+ self._supported_features = 0
+ self._speed_count = 100
+ self._preset_modes = []
+ # the speed_list attribute is deprecated, support will end with release 2021.7
+ self._speed_list = []
@property
def supported_features(self):
"""Flag supported features."""
- return SUPPORT_SET_SPEED
+ return self._supported_features
+
+ # the speed_list attribute is deprecated, support will end with release 2021.7
+ @property
+ def speed_list(self) -> list:
+ """Get the list of available speeds."""
+ return self._speed_list
+
+ @property
+ def speed_count(self):
+ """Return the number of speeds of the fan supported."""
+ return self._speed_count
+
+ @property
+ def preset_modes(self) -> list:
+ """Get the list of available preset modes."""
+ return self._preset_modes
+
+ @property
+ def percentage(self):
+ """Return the percentage based speed of the fan."""
+ return None
+
+ @property
+ def preset_mode(self):
+ """Return the percentage based speed of the fan."""
+ return None
@property
def should_poll(self):
@@ -701,9 +754,14 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity):
**kwargs,
) -> None:
"""Turn the device on."""
+ # Remove the async_set_speed call is async_set_percentage and async_set_preset_modes have been implemented
if speed:
- # If operation mode was set the device must not be turned on.
- result = await self.async_set_speed(speed)
+ await self.async_set_speed(speed)
+ # If operation mode was set the device must not be turned on.
+ if percentage:
+ await self.async_set_percentage(percentage)
+ if preset_mode:
+ await self.async_set_preset_mode(preset_mode)
else:
result = await self._try_command(
"Turning the miio device on failed.", self._device.on
@@ -771,6 +829,22 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity):
class XiaomiAirPurifier(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Purifier."""
+ PRESET_MODE_MAPPING = {
+ "Auto": AirpurifierOperationMode.Auto,
+ "Silent": AirpurifierOperationMode.Silent,
+ "Favorite": AirpurifierOperationMode.Favorite,
+ "Idle": AirpurifierOperationMode.Favorite,
+ }
+
+ SPEED_MODE_MAPPING = {
+ 1: AirpurifierOperationMode.Silent,
+ 2: AirpurifierOperationMode.Medium,
+ 3: AirpurifierOperationMode.High,
+ 4: AirpurifierOperationMode.Strong,
+ }
+
+ REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()}
+
def __init__(self, name, device, entry, unique_id, allowed_failures=0):
"""Initialize the plug switch."""
super().__init__(name, device, entry, unique_id)
@@ -780,26 +854,60 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
if self._model == MODEL_AIRPURIFIER_PRO:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO
+ # SUPPORT_SET_SPEED was disabled
+ # the device supports preset_modes only
+ self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO
+ self._supported_features = SUPPORT_PRESET_MODE
+ self._speed_count = 1
+ # the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO
elif self._model == MODEL_AIRPURIFIER_PRO_V7:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7
+ # SUPPORT_SET_SPEED was disabled
+ # the device supports preset_modes only
+ self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO_V7
+ self._supported_features = SUPPORT_PRESET_MODE
+ self._speed_count = 1
+ # the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO_V7
elif self._model in [MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_2H]:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S
+ # SUPPORT_SET_SPEED was disabled
+ # the device supports preset_modes only
+ self._preset_modes = PRESET_MODES_AIRPURIFIER_2S
+ self._supported_features = SUPPORT_PRESET_MODE
+ self._speed_count = 1
+ # the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = OPERATION_MODES_AIRPURIFIER_2S
elif self._model in MODELS_PURIFIER_MIOT:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_3
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_3
+ # SUPPORT_SET_SPEED was disabled
+ # the device supports preset_modes only
+ self._preset_modes = PRESET_MODES_AIRPURIFIER_3
+ self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE
+ self._speed_count = 3
+ # the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = OPERATION_MODES_AIRPURIFIER_3
elif self._model == MODEL_AIRPURIFIER_V3:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3
+ # SUPPORT_SET_SPEED was disabled
+ # the device supports preset_modes only
+ self._preset_modes = PRESET_MODES_AIRPURIFIER_V3
+ self._supported_features = SUPPORT_PRESET_MODE
+ self._speed_count = 1
+ # the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = OPERATION_MODES_AIRPURIFIER_V3
else:
self._device_features = FEATURE_FLAGS_AIRPURIFIER
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER
+ self._preset_modes = PRESET_MODES_AIRPURIFIER
+ self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE
+ self._speed_count = 4
+ # the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = OPERATION_MODES_AIRPURIFIER
self._state_attrs.update(
@@ -846,10 +954,27 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
)
@property
- def speed_list(self) -> list:
- """Get the list of available speeds."""
- return self._speed_list
+ def preset_mode(self):
+ """Get the active preset mode."""
+ if self._state:
+ preset_mode = AirpurifierOperationMode(self._state_attrs[ATTR_MODE]).name
+ return preset_mode if preset_mode in self._preset_modes else None
+ return None
+
+ @property
+ def percentage(self):
+ """Return the current percentage based speed."""
+ if self._state:
+ mode = AirpurifierOperationMode(self._state_attrs[ATTR_MODE])
+ if mode in self.REVERSE_SPEED_MODE_MAPPING:
+ return ranged_value_to_percentage(
+ (1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode]
+ )
+
+ return None
+
+ # the speed attribute is deprecated, support will end with release 2021.7
@property
def speed(self):
"""Return the current speed."""
@@ -858,6 +983,37 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
return None
+ async def async_set_percentage(self, percentage: int) -> None:
+ """Set the percentage of the fan.
+
+ This method is a coroutine.
+ """
+ speed_mode = math.ceil(
+ percentage_to_ranged_value((1, self._speed_count), percentage)
+ )
+ if speed_mode:
+ await self._try_command(
+ "Setting operation mode of the miio device failed.",
+ self._device.set_mode,
+ AirpurifierOperationMode(self.SPEED_MODE_MAPPING[speed_mode]),
+ )
+
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
+ """Set the preset mode of the fan.
+
+ 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
+ await self._try_command(
+ "Setting operation mode of the miio device failed.",
+ self._device.set_mode,
+ self.PRESET_MODE_MAPPING[preset_mode],
+ )
+
+ # the async_set_speed function is deprecated, support will end with release 2021.7
+ # it is added here only for compatibility with legacy speeds
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
if self.supported_features & SUPPORT_SET_SPEED == 0:
@@ -1004,6 +1160,34 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
class XiaomiAirPurifierMiot(XiaomiAirPurifier):
"""Representation of a Xiaomi Air Purifier (MiOT protocol)."""
+ PRESET_MODE_MAPPING = {
+ "Auto": AirpurifierMiotOperationMode.Auto,
+ "Silent": AirpurifierMiotOperationMode.Silent,
+ "Favorite": AirpurifierMiotOperationMode.Favorite,
+ "Fan": AirpurifierMiotOperationMode.Fan,
+ }
+
+ @property
+ def percentage(self):
+ """Return the current percentage based speed."""
+ if self._state:
+ fan_level = self._state_attrs[ATTR_FAN_LEVEL]
+ return ranged_value_to_percentage((1, 3), fan_level)
+
+ return None
+
+ @property
+ def preset_mode(self):
+ """Get the active preset mode."""
+ if self._state:
+ preset_mode = AirpurifierMiotOperationMode(
+ self._state_attrs[ATTR_MODE]
+ ).name
+ return preset_mode if preset_mode in self._preset_modes else None
+
+ return None
+
+ # the speed attribute is deprecated, support will end with release 2021.7
@property
def speed(self):
"""Return the current speed."""
@@ -1012,6 +1196,35 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier):
return None
+ async def async_set_percentage(self, percentage: int) -> None:
+ """Set the percentage of the fan.
+
+ This method is a coroutine.
+ """
+ fan_level = math.ceil(percentage_to_ranged_value((1, 3), percentage))
+ if fan_level:
+ await self._try_command(
+ "Setting fan level of the miio device failed.",
+ self._device.set_fan_level,
+ fan_level,
+ )
+
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
+ """Set the preset mode of the fan.
+
+ 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
+ await self._try_command(
+ "Setting operation mode of the miio device failed.",
+ self._device.set_mode,
+ self.PRESET_MODE_MAPPING[preset_mode],
+ )
+
+ # the async_set_speed function is deprecated, support will end with release 2021.7
+ # it is added here only for compatibility with legacy speeds
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
if self.supported_features & SUPPORT_SET_SPEED == 0:
@@ -1040,30 +1253,58 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier):
class XiaomiAirHumidifier(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Humidifier."""
+ SPEED_MODE_MAPPING = {
+ 1: AirhumidifierOperationMode.Silent,
+ 2: AirhumidifierOperationMode.Medium,
+ 3: AirhumidifierOperationMode.High,
+ 4: AirhumidifierOperationMode.Strong,
+ }
+
+ REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()}
+
+ PRESET_MODE_MAPPING = {
+ "Auto": AirhumidifierOperationMode.Auto,
+ }
+
def __init__(self, name, device, entry, unique_id):
"""Initialize the plug switch."""
super().__init__(name, device, entry, unique_id)
-
+ self._percentage = None
+ self._preset_mode = None
+ self._supported_features = SUPPORT_SET_SPEED
+ self._preset_modes = []
if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB
+ # the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = [
mode.name
for mode in AirhumidifierOperationMode
if mode is not AirhumidifierOperationMode.Strong
]
+ self._supported_features |= SUPPORT_PRESET_MODE
+ self._preset_modes = PRESET_MODES_AIRHUMIDIFIER
+ self._speed_count = 3
elif self._model in [MODEL_AIRHUMIDIFIER_CA4]:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA4
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA4
+ # the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
+ self._supported_features |= SUPPORT_PRESET_MODE
+ self._preset_modes = PRESET_MODES_AIRHUMIDIFIER
+ self._speed_count = 3
else:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER
+ # the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = [
mode.name
for mode in AirhumidifierOperationMode
if mode is not AirhumidifierOperationMode.Auto
]
+ self._supported_features |= SUPPORT_PRESET_MODE
+ self._preset_modes = PRESET_MODES_AIRHUMIDIFIER
+ self._speed_count = 4
self._state_attrs.update(
{attribute: None for attribute in self._available_attributes}
@@ -1095,10 +1336,27 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
- def speed_list(self) -> list:
- """Get the list of available speeds."""
- return self._speed_list
+ def preset_mode(self):
+ """Get the active preset mode."""
+ if self._state:
+ preset_mode = AirhumidifierOperationMode(self._state_attrs[ATTR_MODE]).name
+ return preset_mode if preset_mode in self._preset_modes else None
+ return None
+
+ @property
+ def percentage(self):
+ """Return the current percentage based speed."""
+ if self._state:
+ mode = AirhumidifierOperationMode(self._state_attrs[ATTR_MODE])
+ if mode in self.REVERSE_SPEED_MODE_MAPPING:
+ return ranged_value_to_percentage(
+ (1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode]
+ )
+
+ return None
+
+ # the speed attribute is deprecated, support will end with release 2021.7
@property
def speed(self):
"""Return the current speed."""
@@ -1107,6 +1365,37 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
return None
+ async def async_set_percentage(self, percentage: int) -> None:
+ """Set the percentage of the fan.
+
+ This method is a coroutine.
+ """
+ speed_mode = math.ceil(
+ percentage_to_ranged_value((1, self._speed_count), percentage)
+ )
+ if speed_mode:
+ await self._try_command(
+ "Setting operation mode of the miio device failed.",
+ self._device.set_mode,
+ AirhumidifierOperationMode(self.SPEED_MODE_MAPPING[speed_mode]),
+ )
+
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
+ """Set the preset mode of the fan.
+
+ 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
+ await self._try_command(
+ "Setting operation mode of the miio device failed.",
+ self._device.set_mode,
+ self.PRESET_MODE_MAPPING[preset_mode],
+ )
+
+ # the async_set_speed function is deprecated, support will end with release 2021.7
+ # it is added here only for compatibility with legacy speeds
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
if self.supported_features & SUPPORT_SET_SPEED == 0:
@@ -1168,21 +1457,64 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
class XiaomiAirHumidifierMiot(XiaomiAirHumidifier):
"""Representation of a Xiaomi Air Humidifier (MiOT protocol)."""
- MODE_MAPPING = {
+ PRESET_MODE_MAPPING = {
+ AirhumidifierMiotOperationMode.Auto: "Auto",
+ }
+
+ REVERSE_PRESET_MODE_MAPPING = {v: k for k, v in PRESET_MODE_MAPPING.items()}
+
+ SPEED_MAPPING = {
AirhumidifierMiotOperationMode.Low: SPEED_LOW,
AirhumidifierMiotOperationMode.Mid: SPEED_MEDIUM,
AirhumidifierMiotOperationMode.High: SPEED_HIGH,
}
- REVERSE_MODE_MAPPING = {v: k for k, v in MODE_MAPPING.items()}
+ REVERSE_SPEED_MAPPING = {v: k for k, v in SPEED_MAPPING.items()}
+ SPEEDS = [
+ AirhumidifierMiotOperationMode.Low,
+ AirhumidifierMiotOperationMode.Mid,
+ AirhumidifierMiotOperationMode.High,
+ ]
+
+ # the speed attribute is deprecated, support will end with release 2021.7
+ # it is added here for compatibility
@property
def speed(self):
- """Return the current speed."""
+ """Return current legacy speed."""
+ if (
+ self.state
+ and AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE])
+ in self.SPEED_MAPPING
+ ):
+ return self.SPEED_MAPPING[
+ AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE])
+ ]
+ return None
+
+ @property
+ def percentage(self):
+ """Return the current percentage based speed."""
+ if (
+ self.state
+ and AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE])
+ in self.SPEEDS
+ ):
+ return ranged_value_to_percentage(
+ (1, self.speed_count), self._state_attrs[ATTR_MODE]
+ )
+
+ return None
+
+ @property
+ def preset_mode(self):
+ """Return the current preset_mode."""
if self._state:
- return self.MODE_MAPPING.get(
+ mode = self.PRESET_MODE_MAPPING.get(
AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE])
)
+ if mode in self._preset_modes:
+ return mode
return None
@@ -1196,12 +1528,42 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier):
return None
+ # the async_set_speed function is deprecated, support will end with release 2021.7
+ # it is added here only for compatibility with legacy speeds
async def async_set_speed(self, speed: str) -> None:
- """Set the speed of the fan."""
+ """Override for set async_set_speed of the super() class."""
+ if speed and speed in self.REVERSE_SPEED_MAPPING:
+ await self._try_command(
+ "Setting operation mode of the miio device failed.",
+ self._device.set_mode,
+ self.REVERSE_SPEED_MAPPING[speed],
+ )
+
+ async def async_set_percentage(self, percentage: int) -> None:
+ """Set the percentage of the fan.
+
+ This method is a coroutine.
+ """
+ mode = math.ceil(percentage_to_ranged_value((1, 3), percentage))
+ if mode:
+ await self._try_command(
+ "Setting operation mode of the miio device failed.",
+ self._device.set_mode,
+ AirhumidifierMiotOperationMode(mode),
+ )
+
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
+ """Set the preset mode of the fan.
+
+ 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
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
- self.REVERSE_MODE_MAPPING[speed],
+ self.REVERSE_PRESET_MODE_MAPPING[preset_mode],
)
async def async_set_led_brightness(self, brightness: int = 2):
@@ -1230,13 +1592,30 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier):
class XiaomiAirFresh(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Fresh."""
+ SPEED_MODE_MAPPING = {
+ 1: AirfreshOperationMode.Silent,
+ 2: AirfreshOperationMode.Low,
+ 3: AirfreshOperationMode.Middle,
+ 4: AirfreshOperationMode.Strong,
+ }
+
+ REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()}
+
+ PRESET_MODE_MAPPING = {
+ "Auto": AirfreshOperationMode.Auto,
+ "Interval": AirfreshOperationMode.Interval,
+ }
+
def __init__(self, name, device, entry, unique_id):
"""Initialize the miio device."""
super().__init__(name, device, entry, unique_id)
self._device_features = FEATURE_FLAGS_AIRFRESH
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH
+ # the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = OPERATION_MODES_AIRFRESH
+ self._speed_count = 4
+ self._preset_modes = PRESET_MODES_AIRFRESH
self._state_attrs.update(
{attribute: None for attribute in self._available_attributes}
)
@@ -1267,10 +1646,27 @@ class XiaomiAirFresh(XiaomiGenericDevice):
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
- def speed_list(self) -> list:
- """Get the list of available speeds."""
- return self._speed_list
+ def preset_mode(self):
+ """Get the active preset mode."""
+ if self._state:
+ preset_mode = AirfreshOperationMode(self._state_attrs[ATTR_MODE]).name
+ return preset_mode if preset_mode in self._preset_modes else None
+ return None
+
+ @property
+ def percentage(self):
+ """Return the current percentage based speed."""
+ if self._state:
+ mode = AirfreshOperationMode(self._state_attrs[ATTR_MODE])
+ if mode in self.REVERSE_SPEED_MODE_MAPPING:
+ return ranged_value_to_percentage(
+ (1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode]
+ )
+
+ return None
+
+ # the speed attribute is deprecated, support will end with release 2021.7
@property
def speed(self):
"""Return the current speed."""
@@ -1279,6 +1675,37 @@ class XiaomiAirFresh(XiaomiGenericDevice):
return None
+ async def async_set_percentage(self, percentage: int) -> None:
+ """Set the percentage of the fan.
+
+ This method is a coroutine.
+ """
+ speed_mode = math.ceil(
+ percentage_to_ranged_value((1, self._speed_count), percentage)
+ )
+ if speed_mode:
+ await self._try_command(
+ "Setting operation mode of the miio device failed.",
+ self._device.set_mode,
+ AirfreshOperationMode(self.SPEED_MODE_MAPPING[speed_mode]),
+ )
+
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
+ """Set the preset mode of the fan.
+
+ 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
+ await self._try_command(
+ "Setting operation mode of the miio device failed.",
+ self._device.set_mode,
+ self.PRESET_MODE_MAPPING[preset_mode],
+ )
+
+ # the async_set_speed function is deprecated, support will end with release 2021.7
+ # it is added here only for compatibility with legacy speeds
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
if self.supported_features & SUPPORT_SET_SPEED == 0:
diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py
index be96f77240a..17f42f4bffa 100644
--- a/homeassistant/components/xiaomi_miio/gateway.py
+++ b/homeassistant/components/xiaomi_miio/gateway.py
@@ -1,12 +1,23 @@
"""Code to handle a Xiaomi Gateway."""
import logging
+from construct.core import ChecksumError
+from micloud import MiCloud
from miio import DeviceException, gateway
+from miio.gateway.gateway import GATEWAY_MODEL_EU
+from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from .const import ATTR_AVAILABLE, DOMAIN
+from .const import (
+ ATTR_AVAILABLE,
+ CONF_CLOUD_COUNTRY,
+ CONF_CLOUD_PASSWORD,
+ CONF_CLOUD_SUBDEVICES,
+ CONF_CLOUD_USERNAME,
+ DOMAIN,
+)
_LOGGER = logging.getLogger(__name__)
@@ -14,11 +25,18 @@ _LOGGER = logging.getLogger(__name__)
class ConnectXiaomiGateway:
"""Class to async connect to a Xiaomi Gateway."""
- def __init__(self, hass):
+ def __init__(self, hass, config_entry):
"""Initialize the entity."""
self._hass = hass
+ self._config_entry = config_entry
self._gateway_device = None
self._gateway_info = None
+ self._use_cloud = None
+ self._cloud_username = None
+ self._cloud_password = None
+ self._cloud_country = None
+ self._host = None
+ self._token = None
@property
def gateway_device(self):
@@ -33,21 +51,17 @@ class ConnectXiaomiGateway:
async def async_connect_gateway(self, host, token):
"""Connect to the Xiaomi Gateway."""
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
- try:
- self._gateway_device = gateway.Gateway(host, token)
- # get the gateway info
- self._gateway_info = await self._hass.async_add_executor_job(
- self._gateway_device.info
- )
- # get the connected sub devices
- await self._hass.async_add_executor_job(
- self._gateway_device.discover_devices
- )
- except DeviceException:
- _LOGGER.error(
- "DeviceException during setup of xiaomi gateway with host %s", host
- )
+
+ self._host = host
+ self._token = token
+ self._use_cloud = self._config_entry.options.get(CONF_CLOUD_SUBDEVICES, False)
+ self._cloud_username = self._config_entry.data.get(CONF_CLOUD_USERNAME)
+ self._cloud_password = self._config_entry.data.get(CONF_CLOUD_PASSWORD)
+ self._cloud_country = self._config_entry.data.get(CONF_CLOUD_COUNTRY)
+
+ if not await self._hass.async_add_executor_job(self.connect_gateway):
return False
+
_LOGGER.debug(
"%s %s %s detected",
self._gateway_info.model,
@@ -56,6 +70,67 @@ class ConnectXiaomiGateway:
)
return True
+ def connect_gateway(self):
+ """Connect the gateway in a way that can called by async_add_executor_job."""
+ try:
+ self._gateway_device = gateway.Gateway(self._host, self._token)
+ # get the gateway info
+ self._gateway_info = self._gateway_device.info()
+ except DeviceException as error:
+ if isinstance(error.__cause__, ChecksumError):
+ raise ConfigEntryAuthFailed(error) from error
+
+ _LOGGER.error(
+ "DeviceException during setup of xiaomi gateway with host %s: %s",
+ self._host,
+ error,
+ )
+ return False
+
+ # get the connected sub devices
+ use_cloud = self._use_cloud or self._gateway_info.model == GATEWAY_MODEL_EU
+ if not use_cloud:
+ # use local query (not supported by all gateway types)
+ try:
+ self._gateway_device.discover_devices()
+ except DeviceException as error:
+ _LOGGER.info(
+ "DeviceException during getting subdevices of xiaomi gateway"
+ " with host %s, trying cloud to obtain subdevices: %s",
+ self._host,
+ error,
+ )
+ use_cloud = True
+
+ if use_cloud:
+ # use miio-cloud
+ if (
+ self._cloud_username is None
+ or self._cloud_password is None
+ or self._cloud_country is None
+ ):
+ raise ConfigEntryAuthFailed(
+ "Missing cloud credentials in Xiaomi Miio configuration"
+ )
+
+ try:
+ miio_cloud = MiCloud(self._cloud_username, self._cloud_password)
+ if not miio_cloud.login():
+ raise ConfigEntryAuthFailed(
+ "Could not login to Xioami Miio Cloud, check the credentials"
+ )
+ devices_raw = miio_cloud.get_devices(self._cloud_country)
+ self._gateway_device.get_devices_from_dict(devices_raw)
+ except DeviceException as error:
+ _LOGGER.error(
+ "DeviceException during setup of xiaomi gateway with host %s: %s",
+ self._host,
+ error,
+ )
+ return False
+
+ return True
+
class XiaomiGatewayDevice(CoordinatorEntity, Entity):
"""Representation of a base Xiaomi Gateway Device."""
diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json
index 939e30edda8..1f37d624b95 100644
--- a/homeassistant/components/xiaomi_miio/manifest.json
+++ b/homeassistant/components/xiaomi_miio/manifest.json
@@ -3,7 +3,7 @@
"name": "Xiaomi Miio",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
- "requirements": ["construct==2.10.56", "python-miio==0.5.6"],
+ "requirements": ["construct==2.10.56", "micloud==0.3", "python-miio==0.5.6"],
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG"],
"zeroconf": ["_miio._udp.local."],
"iot_class": "local_polling"
diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py
index 16ca4d3e7ec..5d271a772b9 100644
--- a/homeassistant/components/xiaomi_miio/sensor.py
+++ b/homeassistant/components/xiaomi_miio/sensor.py
@@ -273,6 +273,9 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity):
class XiaomiGatewayIlluminanceSensor(SensorEntity):
"""Representation of the gateway device's illuminance sensor."""
+ _attr_device_class = DEVICE_CLASS_ILLUMINANCE
+ _attr_unit_of_measurement = UNIT_LUMEN
+
def __init__(self, gateway_device, gateway_name, gateway_device_id):
"""Initialize the entity."""
self._gateway = gateway_device
@@ -302,16 +305,6 @@ class XiaomiGatewayIlluminanceSensor(SensorEntity):
"""Return true when state is known."""
return self._available
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity."""
- return UNIT_LUMEN
-
- @property
- def device_class(self):
- """Return the device class of this entity."""
- return DEVICE_CLASS_ILLUMINANCE
-
@property
def state(self):
"""Return the state of the device."""
diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json
index 571df98eef1..69a1621c973 100644
--- a/homeassistant/components/xiaomi_miio/strings.json
+++ b/homeassistant/components/xiaomi_miio/strings.json
@@ -1,23 +1,70 @@
{
"config": {
"abort": {
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
+ "incomplete_info": "Incomplete information to setup device, no host or token supplied.",
+ "not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "unknown_device": "The device model is not known, not able to setup the device using config flow."
+ "unknown_device": "The device model is not known, not able to setup the device using config flow.",
+ "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.",
+ "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country",
+ "cloud_login_error": "Could not login to Xioami Miio Cloud, check the credentials."
},
"flow_title": "{name}",
"step": {
- "device": {
+ "reauth_confirm": {
+ "description": "The Xiaomi Miio integration needs to re-authenticate your account in order to update the tokens or add missing cloud credentials.",
+ "title": "[%key:common::config_flow::title::reauth%]"
+ },
+ "cloud": {
+ "data": {
+ "cloud_username": "Cloud username",
+ "cloud_password": "Cloud password",
+ "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"
+ },
+ "select": {
+ "data": {
+ "select_device": "Miio device"
+ },
+ "description": "Select the Xiaomi Miio device to setup.",
+ "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
+ },
+ "manual": {
"data": {
"host": "[%key:common::config_flow::data::ip%]",
- "model": "Device model (Optional)",
"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"
+ },
+ "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"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country"
+ },
+ "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/switch.py b/homeassistant/components/xiaomi_miio/switch.py
index 09a9786c372..ace0e52eaea 100644
--- a/homeassistant/components/xiaomi_miio/switch.py
+++ b/homeassistant/components/xiaomi_miio/switch.py
@@ -262,6 +262,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity):
"""Representation of a XiaomiGatewaySwitch."""
+ _attr_device_class = DEVICE_CLASS_SWITCH
+
def __init__(self, coordinator, sub_device, entry, variable):
"""Initialize the XiaomiSensor."""
super().__init__(coordinator, sub_device, entry)
@@ -270,11 +272,6 @@ class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity):
self._unique_id = f"{sub_device.sid}-ch{self._channel}"
self._name = f"{sub_device.name} ch{self._channel} ({sub_device.sid})"
- @property
- def device_class(self):
- """Return the device class of this entity."""
- return DEVICE_CLASS_SWITCH
-
@property
def is_on(self):
"""Return true if switch is on."""
diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json
index 6ee0b1e16fd..ff0a24170f6 100644
--- a/homeassistant/components/xiaomi_miio/translations/ca.json
+++ b/homeassistant/components/xiaomi_miio/translations/ca.json
@@ -2,15 +2,38 @@
"config": {
"abort": {
"already_configured": "[%key::common::config_flow::abort::already_configured_device%]",
- "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs"
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
+ "incomplete_info": "Informaci\u00f3 incompleta per configurar el dispositiu, no s'ha proporcionat cap amfitri\u00f3 o token.",
+ "not_xiaomi_miio": "Xiaomi Miio encara no \u00e9s compatible amb el dispositiu.",
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
},
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3",
+ "cloud_credentials_incomplete": "Credencials del n\u00favol incompletes, introdueix el nom d'usuari, la contrasenya i el pa\u00eds",
+ "cloud_login_error": "No s'ha pogut iniciar sessi\u00f3 a Xioami Miio Cloud, comprova les credencials.",
+ "cloud_no_devices": "No s'han trobat dispositius en aquest compte al n\u00favol de Xiaomi Miio.",
"no_device_selected": "No hi ha cap dispositiu seleccionat, selecciona'n un.",
"unknown_device": "No es reconeix el model del dispositiu, no es pot configurar el dispositiu mitjan\u00e7ant el flux de configuraci\u00f3."
},
"flow_title": "{name}",
"step": {
+ "cloud": {
+ "data": {
+ "cloud_country": "Pa\u00eds del servidor al n\u00favol",
+ "cloud_password": "Contrasenya del n\u00favol",
+ "cloud_username": "Nom d'usuari del n\u00favol",
+ "manual": "Configuraci\u00f3 manual (no recomanada)"
+ },
+ "description": "Inicia sessi\u00f3 al n\u00favol Xiaomi Miio, consulta https://www.openhab.org/addons/bindings/miio/#country-servers per obtenir el servidor al n\u00favol.",
+ "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la Xiaomi"
+ },
+ "connect": {
+ "data": {
+ "model": "Model de dispositiu"
+ },
+ "description": "Selecciona manualment el model de dispositiu entre els models compatibles.",
+ "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la Xiaomi"
+ },
"device": {
"data": {
"host": "Adre\u00e7a IP",
@@ -30,6 +53,25 @@
"description": "Necessitar\u00e0s el Token d'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Tingues en compte que aquest Token d'API \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara.",
"title": "Connexi\u00f3 amb la passarel\u00b7la de Xiaomi"
},
+ "manual": {
+ "data": {
+ "host": "Adre\u00e7a IP",
+ "token": "Token d'API"
+ },
+ "description": "Necessitar\u00e0s el Token d'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/xiaomi_miio/#retrieving-the-access-token. Tingues en compte que aquest Token d'API \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara.",
+ "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la Xiaomi"
+ },
+ "reauth_confirm": {
+ "description": "La integraci\u00f3 Xiaomi Miio ha de tornar a autenticar-se amb el teu compte per poder actualitzar els tokens o afegir credencials pel n\u00favol.",
+ "title": "Reautenticaci\u00f3 de la integraci\u00f3"
+ },
+ "select": {
+ "data": {
+ "select_device": "Dispositiu Miio"
+ },
+ "description": "Selecciona el dispositiu Xiaomi Miio a configurar.",
+ "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la Xiaomi"
+ },
"user": {
"data": {
"gateway": "Connexi\u00f3 amb la passarel\u00b7la de Xiaomi"
@@ -38,5 +80,19 @@
"title": "Xiaomi Miio"
}
}
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "Credencials del n\u00favol incompletes, introdueix el nom d'usuari, la contrasenya i el pa\u00eds"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "cloud_subdevices": "Utilitza el n\u00favol per obtenir subdispositius connectats"
+ },
+ "description": "Especifica par\u00e0metres opcionals",
+ "title": "Xiaomi Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json
index 2817d18b578..7f541180a55 100644
--- a/homeassistant/components/xiaomi_miio/translations/de.json
+++ b/homeassistant/components/xiaomi_miio/translations/de.json
@@ -2,15 +2,38 @@
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
- "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt"
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "incomplete_info": "Unvollst\u00e4ndige Informationen zur Einrichtung des Ger\u00e4ts, kein Host oder Token geliefert.",
+ "not_xiaomi_miio": "Ger\u00e4t wird (noch) nicht von Xiaomi Miio unterst\u00fctzt.",
+ "reauth_successful": "[%key::common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
+ "cloud_credentials_incomplete": "Cloud-Anmeldeinformationen unvollst\u00e4ndig, bitte Benutzernamen, Passwort und Land eingeben",
+ "cloud_login_error": "Konnte sich nicht bei Xioami Miio Cloud anmelden, \u00fcberpr\u00fcfe die Anmeldedaten.",
+ "cloud_no_devices": "Keine Ger\u00e4te in diesem Xiaomi Miio Cloud-Konto gefunden.",
"no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hle ein Ger\u00e4t aus.",
"unknown_device": "Das Ger\u00e4temodell ist nicht bekannt und das Ger\u00e4t kann nicht mithilfe des Assistenten eingerichtet werden."
},
- "flow_title": "Xiaomi Miio: {name}",
+ "flow_title": "{name}",
"step": {
+ "cloud": {
+ "data": {
+ "cloud_country": "Land des Cloud-Servers",
+ "cloud_password": "Cloud-Passwort",
+ "cloud_username": "Cloud-Benutzername",
+ "manual": "Manuell konfigurieren (nicht empfohlen)"
+ },
+ "description": "Melde dich bei der Xiaomi Miio Cloud an, siehe https://www.openhab.org/addons/bindings/miio/#country-servers f\u00fcr den zu verwendenden Cloud-Server.",
+ "title": "Verbinden mit einem Xiaomi Miio Ger\u00e4t oder Xiaomi Gateway"
+ },
+ "connect": {
+ "data": {
+ "model": "Ger\u00e4temodell"
+ },
+ "description": "W\u00e4hle das Ger\u00e4temodell manuell aus den unterst\u00fctzten Modellen aus.",
+ "title": "Verbinden mit einem Xiaomi Miio Ger\u00e4t oder Xiaomi Gateway"
+ },
"device": {
"data": {
"host": "IP-Adresse",
@@ -30,6 +53,25 @@
"description": "Sie ben\u00f6tigen den 32 Zeichen langen API-Token. Anweisungen finden Sie unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.",
"title": "Stelle eine Verbindung zu einem Xiaomi Gateway her"
},
+ "manual": {
+ "data": {
+ "host": "[%key::common::config_flow::data::ip%]",
+ "token": "API-Token"
+ },
+ "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token, siehe https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token f\u00fcr Anweisungen. Bitte beachte, dass sich dieser API-Token von dem Schl\u00fcssel unterscheidet, der von der Xiaomi Aqara-Integration verwendet wird.",
+ "title": "Verbinden mit einem Xiaomi Miio Ger\u00e4t oder Xiaomi Gateway"
+ },
+ "reauth_confirm": {
+ "description": "Die Xiaomi Miio-Integration muss dein Konto neu authentifizieren, um die Token zu aktualisieren oder fehlende Cloud-Anmeldedaten hinzuzuf\u00fcgen.",
+ "title": "[%key::common::config_flow::title::reauth%]"
+ },
+ "select": {
+ "data": {
+ "select_device": "Miio-Ger\u00e4t"
+ },
+ "description": "W\u00e4hle das einzurichtende Xiaomi Miio-Ger\u00e4t aus.",
+ "title": "Verbinden mit einem Xiaomi Miio Ger\u00e4t oder Xiaomi Gateway"
+ },
"user": {
"data": {
"gateway": "Stelle eine Verbindung zu einem Xiaomi Gateway her"
@@ -38,5 +80,19 @@
"title": "Xiaomi Miio"
}
}
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "Cloud-Anmeldeinformationen unvollst\u00e4ndig, bitte Benutzernamen, Passwort und Land eingeben"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "cloud_subdevices": "Cloud verwenden, um verbundene Subdevices zu erhalten"
+ },
+ "description": "Optionale Einstellungen angeben",
+ "title": "Xiaomi Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json
index f5629a86eca..cbe10230093 100644
--- a/homeassistant/components/xiaomi_miio/translations/en.json
+++ b/homeassistant/components/xiaomi_miio/translations/en.json
@@ -2,15 +2,38 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
- "already_in_progress": "Configuration flow is already in progress"
+ "already_in_progress": "Configuration flow is already in progress",
+ "incomplete_info": "Incomplete information to setup device, no host or token supplied.",
+ "not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio.",
+ "reauth_successful": "Re-authentication was successful"
},
"error": {
"cannot_connect": "Failed to connect",
+ "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country",
+ "cloud_login_error": "Could not login to Xioami Miio Cloud, check the credentials.",
+ "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.",
"no_device_selected": "No device selected, please select one device.",
"unknown_device": "The device model is not known, not able to setup the device using config flow."
},
"flow_title": "{name}",
"step": {
+ "cloud": {
+ "data": {
+ "cloud_country": "Cloud server country",
+ "cloud_password": "Cloud password",
+ "cloud_username": "Cloud username",
+ "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"
+ },
+ "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"
+ },
"device": {
"data": {
"host": "IP Address",
@@ -30,6 +53,25 @@
"description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.",
"title": "Connect to a Xiaomi Gateway"
},
+ "manual": {
+ "data": {
+ "host": "IP Address",
+ "token": "API Token"
+ },
+ "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.",
+ "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
+ },
+ "reauth_confirm": {
+ "description": "The Xiaomi Miio integration needs to re-authenticate your account in order to update the tokens or add missing cloud credentials.",
+ "title": "Reauthenticate Integration"
+ },
+ "select": {
+ "data": {
+ "select_device": "Miio device"
+ },
+ "description": "Select the Xiaomi Miio device to setup.",
+ "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
+ },
"user": {
"data": {
"gateway": "Connect to a Xiaomi Gateway"
@@ -38,5 +80,19 @@
"title": "Xiaomi Miio"
}
}
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "cloud_subdevices": "Use cloud to get connected subdevices"
+ },
+ "description": "Specify optional settings",
+ "title": "Xiaomi Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json
index b5ec01007c0..0193cd4a39a 100644
--- a/homeassistant/components/xiaomi_miio/translations/es.json
+++ b/homeassistant/components/xiaomi_miio/translations/es.json
@@ -2,15 +2,37 @@
"config": {
"abort": {
"already_configured": "El dispositivo ya est\u00e1 configurado",
- "already_in_progress": "El flujo de configuraci\u00f3n para este dispositivo Xiaomi Miio ya est\u00e1 en marcha."
+ "already_in_progress": "El flujo de configuraci\u00f3n para este dispositivo Xiaomi Miio ya est\u00e1 en marcha.",
+ "incomplete_info": "Informaci\u00f3n incompleta para configurar el dispositivo, no se ha suministrado ning\u00fan host o token.",
+ "not_xiaomi_miio": "El dispositivo no es (todav\u00eda) compatible con Xiaomi Miio."
},
"error": {
"cannot_connect": "No se pudo conectar",
+ "cloud_credentials_incomplete": "Las credenciales de la nube est\u00e1n incompletas, por favor, rellene el nombre de usuario, la contrase\u00f1a y el pa\u00eds",
+ "cloud_login_error": "No se ha podido iniciar sesi\u00f3n en Xioami Miio Cloud, comprueba las credenciales.",
+ "cloud_no_devices": "No se han encontrado dispositivos en esta cuenta de Xiaomi Miio.",
"no_device_selected": "No se ha seleccionado ning\u00fan dispositivo, por favor, seleccione un dispositivo.",
"unknown_device": "No se conoce el modelo del dispositivo, no se puede configurar el dispositivo mediante el flujo de configuraci\u00f3n."
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "cloud": {
+ "data": {
+ "cloud_country": "Pa\u00eds del servidor de la nube",
+ "cloud_password": "Contrase\u00f1a de la nube",
+ "cloud_username": "Nombre de usuario de la nube",
+ "manual": "Configurar manualmente (no recomendado)"
+ },
+ "description": "Inicie sesi\u00f3n en la nube de Xiaomi Miio, consulte https://www.openhab.org/addons/bindings/miio/#country-servers para conocer el servidor de la nube que debe utilizar.",
+ "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi"
+ },
+ "connect": {
+ "data": {
+ "model": "Modelo del dispositivo"
+ },
+ "description": "Seleccione manualmente el modelo de dispositivo entre los modelos admitidos.",
+ "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi"
+ },
"device": {
"data": {
"host": "Direcci\u00f3n IP",
@@ -30,6 +52,20 @@
"description": "Necesitar\u00e1s el token de la API de 32 caracteres, revisa https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para m\u00e1s instrucciones. Por favor, ten en cuenta que este token es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara.",
"title": "Conectar con un Xiaomi Gateway"
},
+ "manual": {
+ "description": "Necesitar\u00e1 la clave de 32 caracteres Token API, consulte https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token para obtener instrucciones. Tenga en cuenta que esta Token API es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara.",
+ "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi"
+ },
+ "reauth_confirm": {
+ "description": "La integraci\u00f3n de Xiaomi Miio necesita volver a autenticar tu cuenta para actualizar los tokens o a\u00f1adir las credenciales de la nube que faltan."
+ },
+ "select": {
+ "data": {
+ "select_device": "Dispositivo Miio"
+ },
+ "description": "Selecciona el dispositivo Xiaomi Miio para configurarlo.",
+ "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi"
+ },
"user": {
"data": {
"gateway": "Conectar con un Xiaomi Gateway"
@@ -38,5 +74,19 @@
"title": "Xiaomi Miio"
}
}
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "Las credenciales de la nube est\u00e1n incompletas, por favor, rellene el nombre de usuario, la contrase\u00f1a y el pa\u00eds"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "cloud_subdevices": "Utilice la nube para conectar subdispositivos"
+ },
+ "description": "Especifique los ajustes opcionales",
+ "title": "Xiaomi Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json
index acc03463883..92d8ffe048f 100644
--- a/homeassistant/components/xiaomi_miio/translations/et.json
+++ b/homeassistant/components/xiaomi_miio/translations/et.json
@@ -2,15 +2,38 @@
"config": {
"abort": {
"already_configured": "Seade on juba h\u00e4\u00e4lestatud",
- "already_in_progress": "Seadistamine on juba k\u00e4imas"
+ "already_in_progress": "Seadistamine on juba k\u00e4imas",
+ "incomplete_info": "Puudulik seadistusteave, hosti v\u00f5i p\u00e4\u00e4suluba pole esitatud.",
+ "not_xiaomi_miio": "Seade ei ole (veel) Xiaomi Miio poolt toetatud.",
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
},
"error": {
"cannot_connect": "\u00dchendus nurjus",
+ "cloud_credentials_incomplete": "Pilve mandaat on poolik, palun t\u00e4ida kasutajanimi, salas\u00f5na ja riik",
+ "cloud_login_error": "Xioami Miio Cloudi ei saanud sisse logida, kontrolli mandaati.",
+ "cloud_no_devices": "Xiaomi Miio pilvekontolt ei leitud \u00fchtegi seadet.",
"no_device_selected": "Seadmeid pole valitud, vali \u00fcks seade.",
"unknown_device": "Seadme mudel pole teada, seadet ei saa seadistamisvoo abil seadistada."
},
"flow_title": "{name}",
"step": {
+ "cloud": {
+ "data": {
+ "cloud_country": "Pilveserveri riik",
+ "cloud_password": "Pilve salas\u00f5na",
+ "cloud_username": "Pilve kasutajatunnus",
+ "manual": "Seadista k\u00e4sitsi (pole soovitatav)"
+ },
+ "description": "Logi sisse Xiaomi Miio pilve, vaata https://www.openhab.org/addons/bindings/miio/#country-servers pilveserveri kasutamiseks.",
+ "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway"
+ },
+ "connect": {
+ "data": {
+ "model": "Seadme mudel"
+ },
+ "description": "Vali seadme mudel k\u00e4sitsi toetatud mudelite hulgast.",
+ "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway"
+ },
"device": {
"data": {
"host": "IP-aadress",
@@ -30,6 +53,25 @@
"description": "On vaja 32-kohalist API-tokenti, juhiste saamiseks vaata lehte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Pane t\u00e4hele, et see token erineb Xiaomi Aqara sidumisel kasutatavast v\u00f5tmest.",
"title": "Loo \u00fchendus Xiaomi l\u00fc\u00fcsiga"
},
+ "manual": {
+ "data": {
+ "host": "IP aadress",
+ "token": "API v\u00f5ti"
+ },
+ "description": "On vajalik 32 t\u00e4hem\u00e4rki API v\u00f5ti, vt. https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token juhiseid. Pane t\u00e4hele, et see API v\u00f5ti erineb Xiaomi Aqara sidumises kasutatavast v\u00f5tmest.",
+ "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway"
+ },
+ "reauth_confirm": {
+ "description": "Xiaomi Miio sidumine peab konto uuesti tuvastama, et v\u00e4rskendada p\u00e4\u00e4sulube v\u00f5i lisada puuduv pilvemandaat.",
+ "title": "Taastuvastamine"
+ },
+ "select": {
+ "data": {
+ "select_device": "Miio seade"
+ },
+ "description": "Vali seadistamiseks Xiaomi Miio seade.",
+ "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway"
+ },
"user": {
"data": {
"gateway": "Loo \u00fchendus Xiaomi l\u00fc\u00fcsiga"
@@ -38,5 +80,19 @@
"title": "Xiaomi Miio"
}
}
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "Pilve mandaat on poolik, palun t\u00e4ida kasutajanimi, salas\u00f5na ja riik"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "cloud_subdevices": "\u00dchendatud alamseadmete hankimiseks kasuta pilve"
+ },
+ "description": "Valikuliste s\u00e4tete m\u00e4\u00e4ramine",
+ "title": "Xiaomi Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json
new file mode 100644
index 00000000000..9eb4ffc0bb7
--- /dev/null
+++ b/homeassistant/components/xiaomi_miio/translations/he.json
@@ -0,0 +1,37 @@
+{
+ "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",
+ "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"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "device": {
+ "data": {
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API"
+ }
+ },
+ "gateway": {
+ "data": {
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API"
+ },
+ "description": "\u05d0\u05ea\u05d4 \u05d6\u05e7\u05d5\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API , \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05db\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara."
+ },
+ "manual": {
+ "data": {
+ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP",
+ "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API"
+ }
+ },
+ "reauth_confirm": {
+ "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json
index e5cf4501608..d8bf9dfd866 100644
--- a/homeassistant/components/xiaomi_miio/translations/hu.json
+++ b/homeassistant/components/xiaomi_miio/translations/hu.json
@@ -2,13 +2,14 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
- "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van."
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"no_device_selected": "Nincs kiv\u00e1lasztva eszk\u00f6z, k\u00e9rj\u00fck, v\u00e1lasszon egyet."
},
- "flow_title": "Xiaomi Miio: {name}",
+ "flow_title": "{name}",
"step": {
"device": {
"data": {
@@ -29,6 +30,20 @@
"description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. K\u00e9rj\u00fck, vegye figyelembe, hogy ez az API Token k\u00fcl\u00f6nb\u00f6zik a Xiaomi Aqara integr\u00e1ci\u00f3 \u00e1ltal haszn\u00e1lt kulcst\u00f3l.",
"title": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez"
},
+ "manual": {
+ "data": {
+ "host": "IP c\u00edm",
+ "token": "API Token"
+ }
+ },
+ "reauth_confirm": {
+ "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se"
+ },
+ "select": {
+ "data": {
+ "select_device": "Miio eszk\u00f6z"
+ }
+ },
"user": {
"data": {
"gateway": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez"
@@ -37,5 +52,12 @@
"title": "Xiaomi Miio"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Xiaomi Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json
index 8eb851cda5d..ba53ea170fe 100644
--- a/homeassistant/components/xiaomi_miio/translations/it.json
+++ b/homeassistant/components/xiaomi_miio/translations/it.json
@@ -2,15 +2,38 @@
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
- "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso"
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
+ "incomplete_info": "Informazioni incomplete per configurare il dispositivo, nessun host o token fornito.",
+ "not_xiaomi_miio": "Il dispositivo non \u00e8 (ancora) supportato da Xiaomi Miio.",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"error": {
"cannot_connect": "Impossibile connettersi",
+ "cloud_credentials_incomplete": "Credenziali cloud incomplete, inserisci nome utente, password e paese",
+ "cloud_login_error": "Impossibile accedere a Xioami Miio Cloud, controlla le credenziali.",
+ "cloud_no_devices": "Nessun dispositivo trovato in questo account cloud Xiaomi Miio.",
"no_device_selected": "Nessun dispositivo selezionato, selezionare un dispositivo.",
"unknown_device": "Il modello del dispositivo non \u00e8 noto, non \u00e8 possibile configurare il dispositivo utilizzando il flusso di configurazione."
},
"flow_title": "{name}",
"step": {
+ "cloud": {
+ "data": {
+ "cloud_country": "Paese del server cloud",
+ "cloud_password": "Password cloud",
+ "cloud_username": "Nome utente cloud",
+ "manual": "Configura manualmente (non consigliato)"
+ },
+ "description": "Accedi al cloud Xiaomi Miio, vedi https://www.openhab.org/addons/bindings/miio/#country-servers per utilizzare il server cloud.",
+ "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway"
+ },
+ "connect": {
+ "data": {
+ "model": "Modello del dispositivo"
+ },
+ "description": "Seleziona manualmente il modello del dispositivo tra i modelli supportati.",
+ "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway"
+ },
"device": {
"data": {
"host": "Indirizzo IP",
@@ -30,6 +53,25 @@
"description": "E' necessaria la Token API di 32 caratteri, vedere https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token per le istruzioni. Notare che questa Token API \u00e8 differente dalla chiave usata dall'integrazione di Xiaomi Aqara.",
"title": "Connessione a un Xiaomi Gateway "
},
+ "manual": {
+ "data": {
+ "host": "Indirizzo IP",
+ "token": "Token API"
+ },
+ "description": "Avrai bisogno dei 32 caratteri del Token API, vedi https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token per istruzioni. Tieni presente che questo Token API \u00e8 diverso dalla chiave utilizzata dall'integrazione Xiaomi Aqara.",
+ "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway"
+ },
+ "reauth_confirm": {
+ "description": "L'integrazione di Xiaomi Miio deve riautenticare il tuo account per aggiornare i token o aggiungere credenziali cloud mancanti.",
+ "title": "Autenticare nuovamente l'integrazione"
+ },
+ "select": {
+ "data": {
+ "select_device": "Dispositivo Miio"
+ },
+ "description": "Seleziona il dispositivo Xiaomi Miio da configurare.",
+ "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway"
+ },
"user": {
"data": {
"gateway": "Connettiti a un Xiaomi Gateway"
@@ -38,5 +80,19 @@
"title": "Xiaomi Miio"
}
}
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "Credenziali cloud incomplete, inserisci nome utente, password e paese"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "cloud_subdevices": "Usa il cloud per connettere i sottodispositivi"
+ },
+ "description": "Specificare le impostazioni opzionali",
+ "title": "Xiaomi Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json
index 012b976bea3..08c64808a3b 100644
--- a/homeassistant/components/xiaomi_miio/translations/nl.json
+++ b/homeassistant/components/xiaomi_miio/translations/nl.json
@@ -2,15 +2,38 @@
"config": {
"abort": {
"already_configured": "Apparaat is al geconfigureerd",
- "already_in_progress": "De configuratiestroom is al aan de gang"
+ "already_in_progress": "De configuratiestroom is al aan de gang",
+ "incomplete_info": "Onvolledige informatie voor het instellen van het apparaat, geen host of token opgegeven.",
+ "not_xiaomi_miio": "Apparaat wordt (nog) niet ondersteund door Xiaomi Miio.",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
"cannot_connect": "Kan geen verbinding maken",
+ "cloud_credentials_incomplete": "Cloud-inloggegevens onvolledig, vul gebruikersnaam, wachtwoord en land in",
+ "cloud_login_error": "Kan niet inloggen op Xioami Miio Cloud, controleer de inloggegevens.",
+ "cloud_no_devices": "Geen apparaten gevonden in dit Xiaomi Miio-cloudaccount.",
"no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft",
"unknown_device": "Het apparaatmodel is niet bekend, niet in staat om het apparaat in te stellen met config flow."
},
"flow_title": "{name}",
"step": {
+ "cloud": {
+ "data": {
+ "cloud_country": "Land van cloudserver",
+ "cloud_password": "Cloud wachtwoord",
+ "cloud_username": "Cloud gebruikersnaam",
+ "manual": "Handmatig configureren (niet aanbevolen)"
+ },
+ "description": "Log in op de Xiaomi Miio-cloud, zie https://www.openhab.org/addons/bindings/miio/#country-servers voor de te gebruiken cloudserver.",
+ "title": "Maak verbinding met een Xiaomi Miio-apparaat of Xiaomi Gateway"
+ },
+ "connect": {
+ "data": {
+ "model": "Apparaatmodel"
+ },
+ "description": "Selecteer handmatig het apparaatmodel uit de ondersteunde modellen.",
+ "title": "Maak verbinding met een Xiaomi Miio-apparaat of Xiaomi Gateway"
+ },
"device": {
"data": {
"host": "IP-adres",
@@ -30,6 +53,25 @@
"description": "U heeft het API-token nodig, zie https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token voor instructies.",
"title": "Maak verbinding met een Xiaomi Gateway"
},
+ "manual": {
+ "data": {
+ "host": "IP-adres",
+ "token": "API-token"
+ },
+ "description": "U hebt het 32-teken API-token , zie https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token voor instructies. Houd er rekening mee dat deze API-token verschilt van de sleutel die wordt gebruikt door de Xiaomi Aqara-integratie.",
+ "title": "Maak verbinding met een Xiaomi Miio-apparaat of Xiaomi Gateway"
+ },
+ "reauth_confirm": {
+ "description": "De Xiaomi Miio-integratie moet uw account opnieuw verifi\u00ebren om de tokens bij te werken of ontbrekende cloudreferenties toe te voegen.",
+ "title": "Verifieer de integratie opnieuw"
+ },
+ "select": {
+ "data": {
+ "select_device": "Miio-apparaat"
+ },
+ "description": "Selecteer het Xiaomi Miio apparaat dat u wilt instellen.",
+ "title": "Maak verbinding met een Xiaomi Miio-apparaat of Xiaomi-gateway"
+ },
"user": {
"data": {
"gateway": "Maak verbinding met een Xiaomi Gateway"
@@ -38,5 +80,19 @@
"title": "Xiaomi Miio"
}
}
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "Cloud-inloggegevens onvolledig, vul gebruikersnaam, wachtwoord en land in"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "cloud_subdevices": "Gebruik de cloud om aangesloten subapparaten te krijgen"
+ },
+ "description": "Optionele instellingen opgeven",
+ "title": "Xiaomi Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json
index e5ca4d2d004..8fa93169647 100644
--- a/homeassistant/components/xiaomi_miio/translations/no.json
+++ b/homeassistant/components/xiaomi_miio/translations/no.json
@@ -2,15 +2,38 @@
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert",
- "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede"
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
+ "incomplete_info": "Ufullstendig informasjon til installasjonsenheten, ingen vert eller token leveres.",
+ "not_xiaomi_miio": "Enheten st\u00f8ttes (enn\u00e5) ikke av Xiaomi Miio.",
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket"
},
"error": {
"cannot_connect": "Tilkobling mislyktes",
+ "cloud_credentials_incomplete": "Utskriftsinformasjon for skyen er fullstendig. Fyll ut brukernavn, passord og land",
+ "cloud_login_error": "Kunne ikke logge p\u00e5 Xioami Miio Cloud, sjekk legitimasjonen.",
+ "cloud_no_devices": "Ingen enheter funnet i denne Xiaomi Miio-skykontoen.",
"no_device_selected": "Ingen enhet valgt, vennligst velg en enhet.",
"unknown_device": "Enhetsmodellen er ikke kjent, kan ikke konfigurere enheten ved hjelp av konfigurasjonsflyt."
},
"flow_title": "{name}",
"step": {
+ "cloud": {
+ "data": {
+ "cloud_country": "Land for skyserver",
+ "cloud_password": "Passord for sky",
+ "cloud_username": "Brukernavn i skyen",
+ "manual": "Konfigurer manuelt (anbefales ikke)"
+ },
+ "description": "Logg deg p\u00e5 Xiaomi Miio-skyen, se https://www.openhab.org/addons/bindings/miio/#country-servers for skyserveren du kan bruke.",
+ "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway"
+ },
+ "connect": {
+ "data": {
+ "model": "Enhetsmodell"
+ },
+ "description": "Velg enhetsmodellen manuelt fra de st\u00f8ttede modellene.",
+ "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway"
+ },
"device": {
"data": {
"host": "IP adresse",
@@ -30,6 +53,25 @@
"description": "Du trenger 32 tegn API-token , se https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instruksjoner. V\u00e6r oppmerksom p\u00e5 at denne API-token er forskjellig fra n\u00f8kkelen som brukes av Xiaomi Aqara-integrasjonen.",
"title": "Koble til en Xiaomi Gateway"
},
+ "manual": {
+ "data": {
+ "host": "IP adresse",
+ "token": "API-token"
+ },
+ "description": "Du trenger 32 tegn API-token , se https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instruksjoner. V\u00e6r oppmerksom p\u00e5 at denne API-token er forskjellig fra n\u00f8kkelen som brukes av Xiaomi Aqara-integrasjonen.",
+ "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway"
+ },
+ "reauth_confirm": {
+ "description": "Xiaomi Miio-integrasjonen m\u00e5 autentisere kontoen din p\u00e5 nytt for \u00e5 oppdatere tokens eller legge til manglende skylegitimasjon.",
+ "title": "Godkjenne integrering p\u00e5 nytt"
+ },
+ "select": {
+ "data": {
+ "select_device": "Miio-enhet"
+ },
+ "description": "Velg Xiaomi Miio-enheten du vil installere.",
+ "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway"
+ },
"user": {
"data": {
"gateway": "Koble til en Xiaomi Gateway"
@@ -38,5 +80,19 @@
"title": ""
}
}
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "Utskriftsinformasjon for skyen er fullstendig. Fyll ut brukernavn, passord og land"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "cloud_subdevices": "Bruk skyen for \u00e5 f\u00e5 tilkoblede underenheter"
+ },
+ "description": "Spesifiser valgfrie innstillinger",
+ "title": "Xiaomi Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json
index a7c01ef346e..dacb0f3f3ec 100644
--- a/homeassistant/components/xiaomi_miio/translations/pl.json
+++ b/homeassistant/components/xiaomi_miio/translations/pl.json
@@ -2,15 +2,38 @@
"config": {
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
- "already_in_progress": "Konfiguracja jest ju\u017c w toku"
+ "already_in_progress": "Konfiguracja jest ju\u017c w toku",
+ "incomplete_info": "Niepe\u0142ne informacje do skonfigurowania urz\u0105dzenia, brak nazwy hosta, IP lub tokena.",
+ "not_xiaomi_miio": "Urz\u0105dzenie nie jest (jeszcze) obs\u0142ugiwane przez Xiaomi Miio.",
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "cloud_credentials_incomplete": "Dane logowania do chmury niekompletne, prosz\u0119 poda\u0107 nazw\u0119 u\u017cytkownika, has\u0142o i kraj",
+ "cloud_login_error": "Nie mo\u017cna zalogowa\u0107 si\u0119 do chmury Xioami Miio, sprawd\u017a po\u015bwiadczenia.",
+ "cloud_no_devices": "Na tym koncie Xiaomi Miio nie znaleziono \u017cadnych urz\u0105dze\u0144.",
"no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie",
"unknown_device": "Model urz\u0105dzenia nie jest znany, nie mo\u017cna skonfigurowa\u0107 urz\u0105dzenia przy u\u017cyciu interfejsu u\u017cytkownika."
},
"flow_title": "{name}",
"step": {
+ "cloud": {
+ "data": {
+ "cloud_country": "Kraj serwera w chmurze",
+ "cloud_password": "Has\u0142o do chmury",
+ "cloud_username": "Nazwa u\u017cytkownika do chmury",
+ "manual": "Skonfiguruj r\u0119cznie (niezalecane)"
+ },
+ "description": "Zaloguj si\u0119 do chmury Xiaomi Miio, zobacz https://www.openhab.org/addons/bindings/miio/#country-servers, aby zobaczy\u0107, kt\u00f3rego serwera u\u017cy\u0107.",
+ "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio"
+ },
+ "connect": {
+ "data": {
+ "model": "Model urz\u0105dzenia"
+ },
+ "description": "Wybierz r\u0119cznie model urz\u0105dzenia z listy obs\u0142ugiwanych modeli.",
+ "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio"
+ },
"device": {
"data": {
"host": "Adres IP",
@@ -19,7 +42,7 @@
"token": "Token API"
},
"description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32 znaki), odwied\u017a https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Zauwa\u017c i\u017c jest to inny token ni\u017c w integracji Xiaomi Aqara.",
- "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi b\u0105d\u017a innym urz\u0105dzeniem Xiaomi Miio"
+ "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio"
},
"gateway": {
"data": {
@@ -30,6 +53,25 @@
"description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32 znaki), odwied\u017a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Zauwa\u017c i\u017c jest to inny token ni\u017c w integracji Xiaomi Aqara .",
"title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi"
},
+ "manual": {
+ "data": {
+ "host": "Adres IP",
+ "token": "Token API"
+ },
+ "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32-znaki), zobacz https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Pami\u0119taj, \u017ce ten token API r\u00f3\u017cni si\u0119 od klucza u\u017cywanego przez integracj\u0119 Xiaomi Aqara.",
+ "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio"
+ },
+ "reauth_confirm": {
+ "description": "Integracja Xiaomi Miio wymaga ponownego uwierzytelnienia Twoje konta, aby zaktualizowa\u0107 tokeny lub doda\u0107 brakuj\u0105ce dane uwierzytelniaj\u0105ce do chmury.",
+ "title": "Ponownie uwierzytelnij integracj\u0119"
+ },
+ "select": {
+ "data": {
+ "select_device": "Urz\u0105dzenie Miio"
+ },
+ "description": "Wybierz urz\u0105dzenie Xiaomi Miio do skonfigurowania.",
+ "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio"
+ },
"user": {
"data": {
"gateway": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi"
@@ -38,5 +80,19 @@
"title": "Xiaomi Miio"
}
}
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "Dane logowania do chmury niekompletne, prosz\u0119 poda\u0107 nazw\u0119 u\u017cytkownika, has\u0142o i kraj"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "cloud_subdevices": "U\u017cyj chmury, aby uzyska\u0107 pod\u0142\u0105czone podurz\u0105dzenia"
+ },
+ "description": "Ustawienia opcjonalne",
+ "title": "Xiaomi Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json
index ef729f33a1e..f9aeb824b20 100644
--- a/homeassistant/components/xiaomi_miio/translations/ru.json
+++ b/homeassistant/components/xiaomi_miio/translations/ru.json
@@ -2,15 +2,38 @@
"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."
+ "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.",
+ "incomplete_info": "\u041d\u0435\u043f\u043e\u043b\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 \u0442\u043e\u043a\u0435\u043d.",
+ "not_xiaomi_miio": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (\u043f\u043e\u043a\u0430) \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f Xiaomi Miio.",
+ "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.",
+ "cloud_credentials_incomplete": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043e\u0431\u043b\u0430\u043a\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u0435. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u043f\u0430\u0440\u043e\u043b\u044c \u0438 \u0441\u0442\u0440\u0430\u043d\u0443.",
+ "cloud_login_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Xioami Miio Cloud, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
+ "cloud_no_devices": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Xiaomi Miio \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b.",
"no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.",
"unknown_device": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430, \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0430\u0441\u0442\u0435\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438."
},
"flow_title": "{name}",
"step": {
+ "cloud": {
+ "data": {
+ "cloud_country": "\u0421\u0442\u0440\u0430\u043d\u0430 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430",
+ "cloud_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430",
+ "cloud_username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430",
+ "manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u0440\u0443\u0447\u043d\u0443\u044e (\u043d\u0435 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f)"
+ },
+ "description": "\u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u043e\u0431\u043b\u0430\u043a\u043e Xiaomi Miio, \u0441\u043c. https://www.openhab.org/addons/bindings/miio/#country-servers \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430.",
+ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi"
+ },
+ "connect": {
+ "data": {
+ "model": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438\u0437 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445.",
+ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi"
+ },
"device": {
"data": {
"host": "IP-\u0430\u0434\u0440\u0435\u0441",
@@ -30,6 +53,25 @@
"description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u0422\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u0442\u043e\u043a\u0435\u043d \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043b\u044e\u0447\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Xiaomi Aqara.",
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u043b\u044e\u0437\u0443 Xiaomi"
},
+ "manual": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441",
+ "token": "\u0422\u043e\u043a\u0435\u043d API"
+ },
+ "description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u0422\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u0442\u043e\u043a\u0435\u043d \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043b\u044e\u0447\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Xiaomi Aqara.",
+ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi"
+ },
+ "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\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Xiaomi Miio, \u0447\u0442\u043e\u0431\u044b \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d\u044b \u0438\u043b\u0438 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u043e\u0431\u043b\u0430\u043a\u0430.",
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
+ },
+ "select": {
+ "data": {
+ "select_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Miio"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Xiaomi Miio \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.",
+ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi"
+ },
"user": {
"data": {
"gateway": "\u0428\u043b\u044e\u0437 Xiaomi"
@@ -38,5 +80,19 @@
"title": "Xiaomi Miio"
}
}
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043e\u0431\u043b\u0430\u043a\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u0435. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u043f\u0430\u0440\u043e\u043b\u044c \u0438 \u0441\u0442\u0440\u0430\u043d\u0443."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "cloud_subdevices": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u043b\u0430\u043a\u043e \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432"
+ },
+ "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
+ "title": "Xiaomi Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json
index 1f84cd3de5a..0034f73fbf3 100644
--- a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json
+++ b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json
@@ -2,14 +2,47 @@
"config": {
"abort": {
"already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86",
- "already_in_progress": "\u6b64\u5c0f\u7c73\u8bbe\u5907\u7684\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d\u3002"
+ "already_in_progress": "\u6b64\u5c0f\u7c73\u8bbe\u5907\u7684\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d\u3002",
+ "incomplete_info": "\u8bbe\u5907\u4fe1\u606f\u4e0d\u5b8c\u6574\uff0c\u672a\u63d0\u4f9b IP \u6216 token\u3002",
+ "not_xiaomi_miio": "Xiaomi Miio \u6682\u672a\u9002\u914d\u8be5\u8bbe\u5907\u3002"
},
"error": {
"cannot_connect": "\u8fde\u63a5\u5931\u8d25",
- "no_device_selected": "\u672a\u9009\u62e9\u8bbe\u5907\uff0c\u8bf7\u9009\u62e9\u4e00\u4e2a\u8bbe\u5907\u3002"
+ "cloud_credentials_incomplete": "\u4e91\u7aef\u51ed\u636e\u4e0d\u5b8c\u6574\uff0c\u8bf7\u8f93\u5165\u7528\u6237\u540d\u3001\u5bc6\u7801\u548c\u56fd\u5bb6/\u5730\u533a",
+ "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"
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "cloud": {
+ "data": {
+ "cloud_country": "\u4e91\u670d\u52a1\u56fd\u5bb6/\u5730\u533a",
+ "cloud_password": "\u5bc6\u7801",
+ "cloud_username": "\u7528\u6237\u540d",
+ "manual": "\u624b\u52a8\u914d\u7f6e\uff08\u4e0d\u63a8\u8350\uff09"
+ },
+ "description": "\u767b\u5f55\u5c0f\u7c73\u4e91\u670d\u52a1\u3002\u6709\u5173\u56fd\u5bb6/\u5730\u533a\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.openhab.org/addons/bindings/miio/#country-servers \u3002",
+ "title": "\u8fde\u63a5\u5230\u5c0f\u7c73 Miio \u8bbe\u5907\u6216\u5c0f\u7c73\u7f51\u5173"
+ },
+ "connect": {
+ "data": {
+ "model": "\u8bbe\u5907 model"
+ },
+ "description": "\u4ece\u652f\u6301\u7684\u578b\u53f7\u4e2d\u624b\u52a8\u9009\u62e9\u3002",
+ "title": "\u8fde\u63a5\u5230\u5c0f\u7c73 Miio \u8bbe\u5907\u6216\u5c0f\u7c73\u7f51\u5173"
+ },
+ "device": {
+ "data": {
+ "host": "IP \u5730\u5740",
+ "model": "\u8bbe\u5907 model\uff08\u53ef\u9009\uff09",
+ "name": "\u8bbe\u5907\u540d\u79f0",
+ "token": "API Token"
+ },
+ "description": "\u60a8\u9700\u8981\u83b7\u53d6\u4e00\u4e2a 32 \u4f4d\u7684 API Token\u3002\u5982\u9700\u5e2e\u52a9\uff0c\u8bf7\u53c2\u9605\u4ee5\u4e0b\u94fe\u63a5: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u3002\u8bf7\u6ce8\u610f\u6b64 token \u4e0d\u540c\u4e8e\u201cXiaomi Aqara\u201d\u96c6\u6210\u6240\u9700\u7684 key\u3002",
+ "title": "\u8fde\u63a5\u5230\u5c0f\u7c73 Miio \u8bbe\u5907\u6216\u5c0f\u7c73\u7f51\u5173"
+ },
"gateway": {
"data": {
"host": "IP \u5730\u5740",
@@ -19,6 +52,22 @@
"description": "\u60a8\u9700\u8981\u83b7\u53d6\u4e00\u4e2a 32 \u4f4d\u7684 API Token\u3002\u5982\u9700\u5e2e\u52a9\uff0c\u8bf7\u53c2\u9605\u4ee5\u4e0b\u94fe\u63a5: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u3002\u8bf7\u6ce8\u610f\u6b64 token \u4e0d\u540c\u4e8e\u201cXiaomi Aqara\u201d\u96c6\u6210\u6240\u9700\u7684 key\u3002",
"title": "\u8fde\u63a5\u5230\u5c0f\u7c73\u7f51\u5173"
},
+ "manual": {
+ "data": {
+ "host": "IP \u5730\u5740",
+ "token": "API Token"
+ }
+ },
+ "reauth_confirm": {
+ "description": "\u5c0f\u7c73 Miio \u96c6\u6210\u9700\u8981\u91cd\u65b0\u9a8c\u8bc1\u60a8\u7684\u5e10\u6237\uff0c\u4ee5\u4fbf\u66f4\u65b0 token \u6216\u6dfb\u52a0\u4e22\u5931\u7684\u4e91\u7aef\u51ed\u636e\u3002"
+ },
+ "select": {
+ "data": {
+ "select_device": "Miio \u8bbe\u5907"
+ },
+ "description": "\u9009\u62e9\u8981\u6dfb\u52a0\u7684\u5c0f\u7c73 Miio \u8bbe\u5907\u3002",
+ "title": "\u8fde\u63a5\u5230\u5c0f\u7c73 Miio \u8bbe\u5907\u6216\u5c0f\u7c73\u7f51\u5173"
+ },
"user": {
"data": {
"gateway": "\u8fde\u63a5\u5230\u5c0f\u7c73\u7f51\u5173"
@@ -27,5 +76,19 @@
"title": "Xiaomi Miio"
}
}
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "\u4e91\u7aef\u51ed\u636e\u4e0d\u5b8c\u6574\uff0c\u8bf7\u8f93\u5165\u7528\u6237\u540d\u3001\u5bc6\u7801\u548c\u56fd\u5bb6/\u5730\u533a"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "cloud_subdevices": "\u901a\u8fc7\u4e91\u7aef\u83b7\u53d6\u8fde\u63a5\u7684\u5b50\u8bbe\u5907"
+ },
+ "description": "\u6307\u5b9a\u53ef\u9009\u8bbe\u7f6e",
+ "title": "Xiaomi Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json
index c79f6906a45..fdbcde43114 100644
--- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json
+++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json
@@ -2,15 +2,38 @@
"config": {
"abort": {
"already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d"
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
+ "incomplete_info": "\u6240\u63d0\u4f9b\u4e4b\u88dd\u7f6e\u8cc7\u8a0a\u4e0d\u5b8c\u6574\u3001\u7121\u4e3b\u6a5f\u7aef\u6216\u6b0a\u6756\uff0c\u7121\u6cd5\u8a2d\u5b9a\u88dd\u7f6e\u3002",
+ "not_xiaomi_miio": "\u5c0f\u7c73 Miio \uff08\u5c1a\uff09\u4e0d\u652f\u63f4\u8a72\u88dd\u7f6e\u3002",
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "cloud_credentials_incomplete": "\u96f2\u7aef\u6191\u8b49\u672a\u5b8c\u6210\uff0c\u8acb\u586b\u5beb\u4f7f\u7528\u8005\u540d\u7a31\u3001\u5bc6\u78bc\u8207\u570b\u5bb6",
+ "cloud_login_error": "\u7121\u6cd5\u767b\u5165\u5c0f\u7c73 Miio \u96f2\u670d\u52d9\uff0c\u8acb\u6aa2\u67e5\u6191\u8b49\u3002",
+ "cloud_no_devices": "\u5c0f\u7c73 Miio \u96f2\u7aef\u5e33\u865f\u672a\u627e\u5230\u4efb\u4f55\u88dd\u7f6e\u3002",
"no_device_selected": "\u672a\u9078\u64c7\u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u88dd\u7f6e\u3002",
"unknown_device": "\u88dd\u7f6e\u578b\u865f\u672a\u77e5\uff0c\u7121\u6cd5\u4f7f\u7528\u8a2d\u5b9a\u6d41\u7a0b\u3002"
},
"flow_title": "{name}",
"step": {
+ "cloud": {
+ "data": {
+ "cloud_country": "\u96f2\u7aef\u670d\u52d9\u4f3a\u670d\u5668\u570b\u5bb6",
+ "cloud_password": "\u96f2\u7aef\u670d\u52d9\u5bc6\u78bc",
+ "cloud_username": "\u96f2\u7aef\u670d\u52d9\u4f7f\u7528\u8005\u540d\u7a31",
+ "manual": "\u624b\u52d5\u8a2d\u5b9a (\u4e0d\u5efa\u8b70)"
+ },
+ "description": "\u767b\u5165\u81f3\u5c0f\u7c73 Miio \u96f2\u670d\u52d9\uff0c\u8acb\u53c3\u95b1 https://www.openhab.org/addons/bindings/miio/#country-servers \u4ee5\u4e86\u89e3\u9078\u64c7\u54ea\u4e00\u7d44\u96f2\u7aef\u4f3a\u670d\u5668\u3002",
+ "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc"
+ },
+ "connect": {
+ "data": {
+ "model": "\u88dd\u7f6e\u578b\u865f"
+ },
+ "description": "\u5f9e\u652f\u63f4\u7684\u578b\u865f\u4e2d\u624b\u52d5\u9078\u64c7\u88dd\u7f6e\u578b\u865f\u3002",
+ "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc"
+ },
"device": {
"data": {
"host": "IP \u4f4d\u5740",
@@ -30,6 +53,25 @@
"description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u6b0a\u6756\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64API \u6b0a\u6756\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u6b0a\u6756\u4e0d\u540c\u3002",
"title": "\u9023\u7dda\u81f3\u5c0f\u7c73\u7db2\u95dc"
},
+ "manual": {
+ "data": {
+ "host": "IP \u4f4d\u5740",
+ "token": "API \u6b0a\u6756"
+ },
+ "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64 API \u6b0a\u6756\u8207\u5c0f\u7c73 Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u5bc6\u9470\u4e0d\u540c\u3002",
+ "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc"
+ },
+ "reauth_confirm": {
+ "description": "\u5c0f\u7c73 Miio \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f\u3001\u65b9\u80fd\u66f4\u65b0\u6b0a\u6756\u6216\u65b0\u589e\u907a\u5931\u7684\u96f2\u7aef\u6191\u8b49\u3002",
+ "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408"
+ },
+ "select": {
+ "data": {
+ "select_device": "Miio \u88dd\u7f6e"
+ },
+ "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684 \u5c0f\u7c73 Miio \u88dd\u7f6e\u3002",
+ "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc"
+ },
"user": {
"data": {
"gateway": "\u9023\u7dda\u81f3\u5c0f\u7c73\u7db2\u95dc"
@@ -38,5 +80,19 @@
"title": "\u5c0f\u7c73 Miio"
}
}
+ },
+ "options": {
+ "error": {
+ "cloud_credentials_incomplete": "\u96f2\u7aef\u6191\u8b49\u672a\u5b8c\u6210\uff0c\u8acb\u586b\u5beb\u4f7f\u7528\u8005\u540d\u7a31\u3001\u5bc6\u78bc\u8207\u570b\u5bb6"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "cloud_subdevices": "\u4f7f\u7528\u96f2\u7aef\u53d6\u5f97\u9023\u7dda\u5b50\u88dd\u7f6e"
+ },
+ "description": "\u6307\u5b9a\u9078\u9805\u8a2d\u5b9a",
+ "title": "\u5c0f\u7c73 Miio"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py
index d0bfc148594..cdd53e784b3 100644
--- a/homeassistant/components/xiaomi_miio/vacuum.py
+++ b/homeassistant/components/xiaomi_miio/vacuum.py
@@ -507,7 +507,11 @@ class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity):
# Fetch timers separately, see #38285
try:
- self._timers = self._device.timer()
+ # Do not try this if the first fetch timed out.
+ # Two timeouts take longer than 10 seconds and trigger a warning.
+ # See #52353
+ if self._available:
+ self._timers = self._device.timer()
except DeviceException as exc:
_LOGGER.debug(
"Unable to fetch timers, this may happen on some devices: %s", exc
diff --git a/homeassistant/components/yamaha/const.py b/homeassistant/components/yamaha/const.py
index fea962938eb..bcfdc55a511 100644
--- a/homeassistant/components/yamaha/const.py
+++ b/homeassistant/components/yamaha/const.py
@@ -1,4 +1,11 @@
"""Constants for the Yamaha component."""
DOMAIN = "yamaha"
+CURSOR_TYPE_DOWN = "down"
+CURSOR_TYPE_LEFT = "left"
+CURSOR_TYPE_RETURN = "return"
+CURSOR_TYPE_RIGHT = "right"
+CURSOR_TYPE_SELECT = "select"
+CURSOR_TYPE_UP = "up"
SERVICE_ENABLE_OUTPUT = "enable_output"
+SERVICE_MENU_CURSOR = "menu_cursor"
SERVICE_SELECT_SCENE = "select_scene"
diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py
index 3f79be43f6e..147a983b298 100644
--- a/homeassistant/components/yamaha/media_player.py
+++ b/homeassistant/components/yamaha/media_player.py
@@ -31,10 +31,21 @@ from homeassistant.const import (
)
from homeassistant.helpers import config_validation as cv, entity_platform
-from .const import SERVICE_ENABLE_OUTPUT, SERVICE_SELECT_SCENE
+from .const import (
+ CURSOR_TYPE_DOWN,
+ CURSOR_TYPE_LEFT,
+ CURSOR_TYPE_RETURN,
+ CURSOR_TYPE_RIGHT,
+ CURSOR_TYPE_SELECT,
+ CURSOR_TYPE_UP,
+ SERVICE_ENABLE_OUTPUT,
+ SERVICE_MENU_CURSOR,
+ SERVICE_SELECT_SCENE,
+)
_LOGGER = logging.getLogger(__name__)
+ATTR_CURSOR = "cursor"
ATTR_ENABLED = "enabled"
ATTR_PORT = "port"
@@ -45,6 +56,14 @@ CONF_SOURCE_NAMES = "source_names"
CONF_ZONE_IGNORE = "zone_ignore"
CONF_ZONE_NAMES = "zone_names"
+CURSOR_TYPE_MAP = {
+ CURSOR_TYPE_DOWN: rxv.RXV.menu_down.__name__,
+ CURSOR_TYPE_LEFT: rxv.RXV.menu_left.__name__,
+ CURSOR_TYPE_RETURN: rxv.RXV.menu_return.__name__,
+ CURSOR_TYPE_RIGHT: rxv.RXV.menu_right.__name__,
+ CURSOR_TYPE_SELECT: rxv.RXV.menu_sel.__name__,
+ CURSOR_TYPE_UP: rxv.RXV.menu_up.__name__,
+}
DATA_YAMAHA = "yamaha_known_receivers"
DEFAULT_NAME = "Yamaha Receiver"
@@ -164,6 +183,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
{vol.Required(ATTR_ENABLED): cv.boolean, vol.Required(ATTR_PORT): cv.string},
"enable_output",
)
+ # Register Service 'menu_cursor'
+ platform.async_register_entity_service(
+ SERVICE_MENU_CURSOR,
+ {vol.Required(ATTR_CURSOR): vol.In(CURSOR_TYPE_MAP)},
+ YamahaDevice.menu_cursor.__name__,
+ )
class YamahaDevice(MediaPlayerEntity):
@@ -382,6 +407,10 @@ class YamahaDevice(MediaPlayerEntity):
"""Enable or disable an output port.."""
self.receiver.enable_output(port, enabled)
+ def menu_cursor(self, cursor):
+ """Press a menu cursor button."""
+ getattr(self.receiver, CURSOR_TYPE_MAP[cursor])()
+
def set_scene(self, scene):
"""Set the current scene."""
try:
diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml
index fe2b2c66384..8d25d5925c1 100644
--- a/homeassistant/components/yamaha/services.yaml
+++ b/homeassistant/components/yamaha/services.yaml
@@ -19,6 +19,20 @@ enable_output:
required: true
selector:
boolean:
+menu_cursor:
+ name: Menu cursor
+ description: Control the cursor in a menu
+ target:
+ entity:
+ integration: yamaha
+ domain: media_player
+ fields:
+ cursor:
+ name: Cursor
+ description: Name of the cursor key to press ('up', 'down', 'left', 'right', 'select', 'return')
+ example: down
+ selector:
+ text:
select_scene:
name: Select scene
description: "Select a scene on the receiver"
diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py
index bf270b508d9..3a8275e98f0 100644
--- a/homeassistant/components/yamaha_musiccast/__init__.py
+++ b/homeassistant/components/yamaha_musiccast/__init__.py
@@ -1 +1,135 @@
-"""The yamaha_musiccast component."""
+"""The MusicCast integration."""
+from __future__ import annotations
+
+from datetime import timedelta
+import logging
+
+from aiomusiccast import MusicCastConnectionException
+from aiomusiccast.musiccast_device import MusicCastData, MusicCastDevice
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
+
+from .const import BRAND, DOMAIN
+
+PLATFORMS = ["media_player"]
+
+_LOGGER = logging.getLogger(__name__)
+SCAN_INTERVAL = timedelta(seconds=60)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up MusicCast from a config entry."""
+
+ client = MusicCastDevice(entry.data[CONF_HOST], async_get_clientsession(hass))
+ coordinator = MusicCastDataUpdateCoordinator(hass, client=client)
+ await coordinator.async_config_entry_first_refresh()
+
+ hass.data.setdefault(DOMAIN, {})
+ hass.data[DOMAIN][entry.entry_id] = coordinator
+
+ hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+
+ entry.async_on_unload(entry.add_update_listener(async_reload_entry))
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
+ """Reload config entry."""
+ await hass.config_entries.async_reload(entry.entry_id)
+
+
+class MusicCastDataUpdateCoordinator(DataUpdateCoordinator[MusicCastData]):
+ """Class to manage fetching data from the API."""
+
+ def __init__(self, hass: HomeAssistant, client: MusicCastDevice) -> None:
+ """Initialize."""
+ self.musiccast = client
+
+ super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)
+ self.entities: list[MusicCastDeviceEntity] = []
+
+ async def _async_update_data(self) -> MusicCastData:
+ """Update data via library."""
+ try:
+ await self.musiccast.fetch()
+ except MusicCastConnectionException as exception:
+ raise UpdateFailed() from exception
+ return self.musiccast.data
+
+
+class MusicCastEntity(CoordinatorEntity):
+ """Defines a base MusicCast entity."""
+
+ coordinator: MusicCastDataUpdateCoordinator
+
+ def __init__(
+ self,
+ *,
+ name: str,
+ icon: str,
+ coordinator: MusicCastDataUpdateCoordinator,
+ enabled_default: bool = True,
+ ) -> None:
+ """Initialize the MusicCast entity."""
+ super().__init__(coordinator)
+ self._enabled_default = enabled_default
+ self._icon = icon
+ self._name = name
+
+ @property
+ def name(self) -> str:
+ """Return the name of the entity."""
+ return self._name
+
+ @property
+ def icon(self) -> str:
+ """Return the mdi icon of the entity."""
+ return self._icon
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Return if the entity should be enabled when first added to the entity registry."""
+ return self._enabled_default
+
+
+class MusicCastDeviceEntity(MusicCastEntity):
+ """Defines a MusicCast device entity."""
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device information about this MusicCast device."""
+ return DeviceInfo(
+ connections={
+ (CONNECTION_NETWORK_MAC, format_mac(mac))
+ for mac in self.coordinator.data.mac_addresses.values()
+ },
+ identifiers={
+ (
+ DOMAIN,
+ self.coordinator.data.device_id,
+ )
+ },
+ name=self.coordinator.data.network_name,
+ manufacturer=BRAND,
+ model=self.coordinator.data.model_name,
+ sw_version=self.coordinator.data.system_version,
+ )
diff --git a/homeassistant/components/yamaha_musiccast/config_flow.py b/homeassistant/components/yamaha_musiccast/config_flow.py
new file mode 100644
index 00000000000..06bb212e639
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/config_flow.py
@@ -0,0 +1,130 @@
+"""Config flow for MusicCast."""
+from __future__ import annotations
+
+import logging
+from urllib.parse import urlparse
+
+from aiohttp import ClientConnectorError
+from aiomusiccast import MusicCastConnectionException, MusicCastDevice
+import voluptuous as vol
+
+from homeassistant import data_entry_flow
+from homeassistant.components import ssdp
+from homeassistant.config_entries import ConfigFlow
+from homeassistant.const import CONF_HOST
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.typing import ConfigType
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN):
+ """Handle a MusicCast config flow."""
+
+ VERSION = 1
+
+ serial_number: str | None = None
+ host: str
+
+ async def async_step_user(
+ self, user_input: ConfigType | None = None
+ ) -> data_entry_flow.FlowResult:
+ """Handle a flow initiated by the user."""
+ # Request user input, unless we are preparing discovery flow
+ if user_input is None:
+ return self._show_setup_form()
+
+ host = user_input[CONF_HOST]
+ serial_number = None
+
+ errors = {}
+ # Check if device is a MusicCast device
+
+ try:
+ info = await MusicCastDevice.get_device_info(
+ host, async_get_clientsession(self.hass)
+ )
+ except (MusicCastConnectionException, ClientConnectorError):
+ errors["base"] = "cannot_connect"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+ serial_number = info.get("system_id")
+ if serial_number is None:
+ errors["base"] = "no_musiccast_device"
+
+ if not errors:
+ await self.async_set_unique_id(serial_number, raise_on_progress=False)
+ self._abort_if_unique_id_configured()
+
+ return self.async_create_entry(
+ title=host,
+ data={
+ CONF_HOST: host,
+ "serial": serial_number,
+ },
+ )
+
+ return self._show_setup_form(errors)
+
+ def _show_setup_form(
+ self, errors: dict | None = None
+ ) -> data_entry_flow.FlowResult:
+ """Show the setup form to the user."""
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
+ errors=errors or {},
+ )
+
+ async def async_step_ssdp(self, discovery_info) -> data_entry_flow.FlowResult:
+ """Handle ssdp discoveries."""
+ if not await MusicCastDevice.check_yamaha_ssdp(
+ discovery_info[ssdp.ATTR_SSDP_LOCATION], async_get_clientsession(self.hass)
+ ):
+ return self.async_abort(reason="yxc_control_url_missing")
+
+ self.serial_number = discovery_info[ssdp.ATTR_UPNP_SERIAL]
+ self.host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname
+ await self.async_set_unique_id(self.serial_number)
+ self._abort_if_unique_id_configured({CONF_HOST: self.host})
+ self.context.update(
+ {
+ "title_placeholders": {
+ "name": discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, self.host)
+ }
+ }
+ )
+
+ return await self.async_step_confirm()
+
+ async def async_step_confirm(self, user_input=None) -> data_entry_flow.FlowResult:
+ """Allow the user to confirm adding the device."""
+ if user_input is not None:
+ return self.async_create_entry(
+ title=self.host,
+ data={
+ CONF_HOST: self.host,
+ "serial": self.serial_number,
+ },
+ )
+
+ 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
new file mode 100644
index 00000000000..b442a3135b9
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/const.py
@@ -0,0 +1,37 @@
+"""Constants for the MusicCast integration."""
+
+from homeassistant.components.media_player.const import (
+ REPEAT_MODE_ALL,
+ REPEAT_MODE_OFF,
+ REPEAT_MODE_ONE,
+)
+
+DOMAIN = "yamaha_musiccast"
+
+BRAND = "Yamaha Corporation"
+
+# Attributes
+ATTR_IDENTIFIERS = "identifiers"
+ATTR_MANUFACTURER = "manufacturer"
+ATTR_MODEL = "model"
+ATTR_PLAYLIST = "playlist"
+ATTR_PRESET = "preset"
+ATTR_SOFTWARE_VERSION = "sw_version"
+ATTR_MC_LINK = "mc_link"
+ATTR_MAIN_SYNC = "main_sync"
+ATTR_MC_LINK_SOURCES = [ATTR_MC_LINK, ATTR_MAIN_SYNC]
+
+DEFAULT_ZONE = "main"
+HA_REPEAT_MODE_TO_MC_MAPPING = {
+ REPEAT_MODE_OFF: "off",
+ REPEAT_MODE_ONE: "one",
+ REPEAT_MODE_ALL: "all",
+}
+
+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 4a0294f444c..501e3b8a00b 100644
--- a/homeassistant/components/yamaha_musiccast/manifest.json
+++ b/homeassistant/components/yamaha_musiccast/manifest.json
@@ -1,8 +1,19 @@
{
"domain": "yamaha_musiccast",
- "name": "Yamaha MusicCast",
+ "name": "MusicCast",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast",
- "requirements": ["pymusiccast==0.1.6"],
- "codeowners": ["@jalmeroth"],
- "iot_class": "local_polling"
-}
+ "requirements": [
+ "aiomusiccast==0.8.0"
+ ],
+ "ssdp": [
+ {
+ "manufacturer": "Yamaha Corporation"
+ }
+ ],
+ "iot_class": "local_push",
+ "codeowners": [
+ "@vigonotion",
+ "@micha91"
+ ]
+}
\ 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 b21f6d3a3f4..b67aa834008 100644
--- a/homeassistant/components/yamaha_musiccast/media_player.py
+++ b/homeassistant/components/yamaha_musiccast/media_player.py
@@ -1,216 +1,428 @@
-"""Support for Yamaha MusicCast Receivers."""
-import logging
-import socket
+"""Implementation of the musiccast media player."""
+from __future__ import annotations
-import pymusiccast
+import logging
+
+from aiomusiccast import MusicCastGroupException
+from aiomusiccast.features import ZoneFeature
import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
from homeassistant.components.media_player.const import (
- MEDIA_TYPE_MUSIC,
+ REPEAT_MODE_OFF,
+ SUPPORT_GROUPING,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK,
+ SUPPORT_REPEAT_SET,
+ SUPPORT_SELECT_SOUND_MODE,
SUPPORT_SELECT_SOURCE,
+ SUPPORT_SHUFFLE_SET,
SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
)
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_PORT,
STATE_IDLE,
- STATE_ON,
+ STATE_OFF,
STATE_PAUSED,
STATE_PLAYING,
- STATE_UNKNOWN,
)
+from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
-import homeassistant.util.dt as dt_util
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import DiscoveryInfoType, HomeAssistantType
+from homeassistant.util import uuid
+
+from . import MusicCastDataUpdateCoordinator, MusicCastDeviceEntity
+from .const import (
+ ATTR_MAIN_SYNC,
+ ATTR_MC_LINK,
+ DEFAULT_ZONE,
+ DOMAIN,
+ HA_REPEAT_MODE_TO_MC_MAPPING,
+ INTERVAL_SECONDS,
+ MC_REPEAT_MODE_TO_HA_MAPPING,
+ NULL_GROUP,
+)
_LOGGER = logging.getLogger(__name__)
-SUPPORTED_FEATURES = (
- SUPPORT_PLAY
- | SUPPORT_PAUSE
- | SUPPORT_STOP
+MUSIC_PLAYER_BASE_SUPPORT = (
+ SUPPORT_PAUSE
+ | SUPPORT_PLAY
+ | SUPPORT_SHUFFLE_SET
+ | SUPPORT_REPEAT_SET
| SUPPORT_PREVIOUS_TRACK
| SUPPORT_NEXT_TRACK
- | SUPPORT_TURN_ON
- | SUPPORT_TURN_OFF
- | SUPPORT_VOLUME_SET
- | SUPPORT_VOLUME_MUTE
+ | SUPPORT_SELECT_SOUND_MODE
| SUPPORT_SELECT_SOURCE
+ | SUPPORT_STOP
+ | SUPPORT_GROUPING
)
-KNOWN_HOSTS_KEY = "data_yamaha_musiccast"
-INTERVAL_SECONDS = "interval_seconds"
-
-DEFAULT_PORT = 5005
-DEFAULT_INTERVAL = 480
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
- vol.Optional(INTERVAL_SECONDS, default=DEFAULT_INTERVAL): cv.positive_int,
+ vol.Optional(CONF_PORT, default=5000): cv.port,
+ vol.Optional(INTERVAL_SECONDS, default=0): cv.positive_int,
}
)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Yamaha MusicCast platform."""
+async def async_setup_platform(
+ hass: HomeAssistantType,
+ config,
+ async_add_devices: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
+ """Import legacy configurations."""
- known_hosts = hass.data.get(KNOWN_HOSTS_KEY)
- if known_hosts is None:
- known_hosts = hass.data[KNOWN_HOSTS_KEY] = []
- _LOGGER.debug("known_hosts: %s", known_hosts)
-
- host = config.get(CONF_HOST)
- port = config.get(CONF_PORT)
- interval = config.get(INTERVAL_SECONDS)
-
- # Get IP of host to prevent duplicates
- try:
- ipaddr = socket.gethostbyname(host)
- except (OSError) as error:
- _LOGGER.error("Could not communicate with %s:%d: %s", host, port, error)
- return
-
- if [item for item in known_hosts if item[0] == ipaddr]:
- _LOGGER.warning("Host %s:%d already registered", host, port)
- return
-
- if [item for item in known_hosts if item[1] == port]:
- _LOGGER.warning("Port %s:%d already registered", host, port)
- return
-
- reg_host = (ipaddr, port)
- known_hosts.append(reg_host)
-
- try:
- receiver = pymusiccast.McDevice(ipaddr, udp_port=port, mc_interval=interval)
- except pymusiccast.exceptions.YMCInitError as err:
- _LOGGER.error(err)
- receiver = None
-
- if receiver:
- for zone in receiver.zones:
- _LOGGER.debug("Receiver: %s / Port: %d / Zone: %s", receiver, port, zone)
- add_entities([YamahaDevice(receiver, receiver.zones[zone])], True)
+ 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:
- known_hosts.remove(reg_host)
+ _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
+ )
+ )
-class YamahaDevice(MediaPlayerEntity):
- """Representation of a Yamaha MusicCast device."""
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up MusicCast sensor based on a config entry."""
+ coordinator: MusicCastDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
- def __init__(self, recv, zone):
- """Initialize the Yamaha MusicCast device."""
- self._recv = recv
- self._name = recv.name
- self._source = None
- self._source_list = []
- self._zone = zone
- self.mute = False
- self.media_status = None
- self.media_status_received = None
- self.power = STATE_UNKNOWN
- self.status = STATE_UNKNOWN
- self.volume = 0
- self.volume_max = 0
- self._recv.set_yamaha_device(self)
- self._zone.set_yamaha_device(self)
+ name = coordinator.data.network_name
+
+ media_players: list[Entity] = []
+
+ for zone in coordinator.data.zones:
+ zone_name = name if zone == DEFAULT_ZONE else f"{name} {zone}"
+
+ media_players.append(
+ MusicCastMediaPlayer(zone, zone_name, entry.entry_id, coordinator)
+ )
+
+ async_add_entities(media_players)
+
+
+class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity):
+ """The musiccast media player."""
+
+ def __init__(self, zone_id, name, entry_id, coordinator):
+ """Initialize the musiccast device."""
+ self._player_state = STATE_PLAYING
+ self._volume_muted = False
+ self._shuffle = False
+ self._zone_id = zone_id
+
+ super().__init__(
+ name=name,
+ icon="mdi:speaker",
+ coordinator=coordinator,
+ )
+
+ self._volume_min = self.coordinator.data.zones[self._zone_id].min_volume
+ self._volume_max = self.coordinator.data.zones[self._zone_id].max_volume
+
+ self._cur_track = 0
+ self._repeat = REPEAT_MODE_OFF
+ self.coordinator.entities.append(self)
+
+ async def async_added_to_hass(self):
+ """Run when this Entity has been added to HA."""
+ await super().async_added_to_hass()
+ # Sensors should also register callbacks to HA when their state changes
+ self.coordinator.musiccast.register_callback(self.async_write_ha_state)
+ self.coordinator.musiccast.register_group_update_callback(
+ self.update_all_mc_entities
+ )
+
+ async def async_will_remove_from_hass(self):
+ """Entity being removed from hass."""
+ await super().async_will_remove_from_hass()
+ # The opposite of async_added_to_hass. Remove any registered call backs here.
+ self.coordinator.musiccast.remove_callback(self.async_write_ha_state)
@property
- def name(self):
- """Return the name of the device."""
- return f"{self._name} ({self._zone.zone_id})"
+ def should_poll(self):
+ """Push an update after each command."""
+ return False
+
+ @property
+ def ip_address(self):
+ """Return the ip address of the musiccast device."""
+ return self.coordinator.musiccast.ip
+
+ @property
+ def zone_id(self):
+ """Return the zone id of the musiccast device."""
+ return self._zone_id
+
+ @property
+ def _is_netusb(self):
+ return (
+ self.coordinator.data.netusb_input
+ == self.coordinator.data.zones[self._zone_id].input
+ )
+
+ @property
+ def _is_tuner(self):
+ return self.coordinator.data.zones[self._zone_id].input == "tuner"
@property
def state(self):
- """Return the state of the device."""
- if self.power == STATE_ON and self.status != STATE_UNKNOWN:
- return self.status
- return self.power
-
- @property
- def is_volume_muted(self):
- """Boolean if volume is currently muted."""
- return self.mute
+ """Return the state of the player."""
+ if self.coordinator.data.zones[self._zone_id].power == "on":
+ if self._is_netusb and self.coordinator.data.netusb_playback == "pause":
+ return STATE_PAUSED
+ if self._is_netusb and self.coordinator.data.netusb_playback == "stop":
+ return STATE_IDLE
+ return STATE_PLAYING
+ return STATE_OFF
@property
def volume_level(self):
- """Volume level of the media player (0..1)."""
- return self.volume
+ """Return the volume level of the media player (0..1)."""
+ if ZoneFeature.VOLUME in self.coordinator.data.zones[self._zone_id].features:
+ volume = self.coordinator.data.zones[self._zone_id].current_volume
+ return (volume - self._volume_min) / (self._volume_max - self._volume_min)
+ return None
+
+ @property
+ def is_volume_muted(self):
+ """Return boolean if volume is currently muted."""
+ if ZoneFeature.VOLUME in self.coordinator.data.zones[self._zone_id].features:
+ return self.coordinator.data.zones[self._zone_id].mute
+ return None
+
+ @property
+ def shuffle(self):
+ """Boolean if shuffling is enabled."""
+ return (
+ self.coordinator.data.netusb_shuffle == "on" if self._is_netusb else False
+ )
+
+ @property
+ def sound_mode(self):
+ """Return the current sound mode."""
+ return self.coordinator.data.zones[self._zone_id].sound_program
+
+ @property
+ def sound_mode_list(self):
+ """Return a list of available sound modes."""
+ return self.coordinator.data.zones[self._zone_id].sound_program_list
+
+ @property
+ def zone(self):
+ """Return the zone of the media player."""
+ return self._zone_id
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID for this media_player."""
+ return f"{self.coordinator.data.device_id}_{self._zone_id}"
+
+ async def async_turn_on(self):
+ """Turn the media player on."""
+ await self.coordinator.musiccast.turn_on(self._zone_id)
+ self.async_write_ha_state()
+
+ async def async_turn_off(self):
+ """Turn the media player off."""
+ await self.coordinator.musiccast.turn_off(self._zone_id)
+ self.async_write_ha_state()
+
+ async def async_mute_volume(self, mute):
+ """Mute the volume."""
+
+ await self.coordinator.musiccast.mute_volume(self._zone_id, mute)
+ self.async_write_ha_state()
+
+ async def async_set_volume_level(self, volume):
+ """Set the volume level, range 0..1."""
+ await self.coordinator.musiccast.set_volume_level(self._zone_id, volume)
+ self.async_write_ha_state()
+
+ async def async_media_play(self):
+ """Send play command."""
+ if self._is_netusb:
+ await self.coordinator.musiccast.netusb_play()
+ else:
+ raise HomeAssistantError(
+ "Service play is not supported for non NetUSB sources."
+ )
+
+ async def async_media_pause(self):
+ """Send pause command."""
+ if self._is_netusb:
+ await self.coordinator.musiccast.netusb_pause()
+ else:
+ raise HomeAssistantError(
+ "Service pause is not supported for non NetUSB sources."
+ )
+
+ async def async_media_stop(self):
+ """Send stop command."""
+ if self._is_netusb:
+ await self.coordinator.musiccast.netusb_pause()
+ else:
+ raise HomeAssistantError(
+ "Service stop is not supported for non NetUSB sources."
+ )
+
+ async def async_set_shuffle(self, shuffle):
+ """Enable/disable shuffle mode."""
+ if self._is_netusb:
+ await self.coordinator.musiccast.netusb_shuffle(shuffle)
+ else:
+ raise HomeAssistantError(
+ "Service shuffle is not supported for non NetUSB sources."
+ )
+
+ async def async_select_sound_mode(self, sound_mode):
+ """Select sound mode."""
+ await self.coordinator.musiccast.select_sound_mode(self._zone_id, sound_mode)
+
+ @property
+ def media_image_url(self):
+ """Return the image url of current playing media."""
+ if self.is_client and self.group_server != self:
+ return self.group_server.coordinator.musiccast.media_image_url
+ return self.coordinator.musiccast.media_image_url if self._is_netusb else None
+
+ @property
+ def media_title(self):
+ """Return the title of current playing media."""
+ if self._is_netusb:
+ return self.coordinator.data.netusb_track
+ if self._is_tuner:
+ return self.coordinator.musiccast.tuner_media_title
+
+ return None
+
+ @property
+ def media_artist(self):
+ """Return the artist of current playing media (Music track only)."""
+ if self._is_netusb:
+ return self.coordinator.data.netusb_artist
+ if self._is_tuner:
+ return self.coordinator.musiccast.tuner_media_artist
+
+ return None
+
+ @property
+ def media_album_name(self):
+ """Return the album of current playing media (Music track only)."""
+ return self.coordinator.data.netusb_album if self._is_netusb else None
+
+ @property
+ def repeat(self):
+ """Return current repeat mode."""
+ return (
+ MC_REPEAT_MODE_TO_HA_MAPPING.get(self.coordinator.data.netusb_repeat)
+ if self._is_netusb
+ else REPEAT_MODE_OFF
+ )
@property
def supported_features(self):
- """Flag of features that are supported."""
- return SUPPORTED_FEATURES
+ """Flag media player features that are supported."""
+ supported_features = MUSIC_PLAYER_BASE_SUPPORT
+ zone = self.coordinator.data.zones[self._zone_id]
+
+ if ZoneFeature.POWER in zone.features:
+ supported_features |= SUPPORT_TURN_ON | SUPPORT_TURN_OFF
+ if ZoneFeature.VOLUME in zone.features:
+ supported_features |= SUPPORT_VOLUME_SET
+ if ZoneFeature.MUTE in zone.features:
+ supported_features |= SUPPORT_VOLUME_MUTE
+
+ return supported_features
+
+ async def async_media_previous_track(self):
+ """Send previous track command."""
+ if self._is_netusb:
+ await self.coordinator.musiccast.netusb_previous_track()
+ elif self._is_tuner:
+ await self.coordinator.musiccast.tuner_previous_station()
+ else:
+ raise HomeAssistantError(
+ "Service previous track is not supported for non NetUSB or Tuner sources."
+ )
+
+ async def async_media_next_track(self):
+ """Send next track command."""
+ if self._is_netusb:
+ await self.coordinator.musiccast.netusb_next_track()
+ elif self._is_tuner:
+ await self.coordinator.musiccast.tuner_next_station()
+ else:
+ raise HomeAssistantError(
+ "Service next track is not supported for non NetUSB or Tuner sources."
+ )
+
+ async def async_set_repeat(self, repeat):
+ """Enable/disable repeat mode."""
+ if self._is_netusb:
+ await self.coordinator.musiccast.netusb_repeat(
+ HA_REPEAT_MODE_TO_MC_MAPPING.get(repeat, "off")
+ )
+ else:
+ raise HomeAssistantError(
+ "Service set repeat is not supported for non NetUSB sources."
+ )
+
+ async def async_select_source(self, source):
+ """Select input source."""
+ await self.coordinator.musiccast.select_source(self._zone_id, source)
@property
def source(self):
- """Return the current input source."""
- return self._source
+ """Name of the current input source."""
+ return self.coordinator.data.zones[self._zone_id].input
@property
def source_list(self):
"""List of available input sources."""
- return self._source_list
-
- @source_list.setter
- def source_list(self, value):
- """Set source_list attribute."""
- self._source_list = value
-
- @property
- def media_content_type(self):
- """Return the media content type."""
- return MEDIA_TYPE_MUSIC
+ return self.coordinator.data.zones[self._zone_id].input_list
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
- return self.media_status.media_duration if self.media_status else None
+ if self._is_netusb:
+ return self.coordinator.data.netusb_total_time
- @property
- def media_image_url(self):
- """Image url of current playing media."""
- return self.media_status.media_image_url if self.media_status else None
-
- @property
- def media_artist(self):
- """Artist of current playing media, music track only."""
- return self.media_status.media_artist if self.media_status else None
-
- @property
- def media_album(self):
- """Album of current playing media, music track only."""
- return self.media_status.media_album if self.media_status else None
-
- @property
- def media_track(self):
- """Track number of current playing media, music track only."""
- return self.media_status.media_track if self.media_status else None
-
- @property
- def media_title(self):
- """Title of current playing media."""
- return self.media_status.media_title if self.media_status else None
+ return None
@property
def media_position(self):
"""Position of current playing media in seconds."""
- if self.media_status and self.state in [
- STATE_PLAYING,
- STATE_PAUSED,
- STATE_IDLE,
- ]:
- return self.media_status.media_position
+ if self._is_netusb:
+ return self.coordinator.data.netusb_play_time
+
+ return None
@property
def media_position_updated_at(self):
@@ -218,74 +430,368 @@ class YamahaDevice(MediaPlayerEntity):
Returns value from homeassistant.util.dt.utcnow().
"""
- return self.media_status_received if self.media_status else None
+ if self._is_netusb:
+ return self.coordinator.data.netusb_play_time_updated
- def update(self):
- """Get the latest details from the device."""
- _LOGGER.debug("update: %s", self.entity_id)
- self._recv.update_status()
- self._zone.update_status()
+ return None
- def update_hass(self):
- """Push updates to Home Assistant."""
- if self.entity_id:
- _LOGGER.debug("update_hass: pushing updates")
- self.schedule_update_ha_state()
- return True
+ # Group and MusicCast System specific functions/properties
- def turn_on(self):
- """Turn on specified media player or all."""
- _LOGGER.debug("Turn device: on")
- self._zone.set_power(True)
+ @property
+ def is_network_server(self) -> bool:
+ """Return only true if the current entity is a network server and not a main zone with an attached zone2."""
+ return (
+ self.coordinator.data.group_role == "server"
+ and self.coordinator.data.group_id != NULL_GROUP
+ and self._zone_id == self.coordinator.data.group_server_zone
+ )
- def turn_off(self):
- """Turn off specified media player or all."""
- _LOGGER.debug("Turn device: off")
- self._zone.set_power(False)
+ @property
+ def other_zones(self) -> list[MusicCastMediaPlayer]:
+ """Return media player entities of the other zones of this device."""
+ return [
+ entity
+ for entity in self.coordinator.entities
+ if entity != self and isinstance(entity, MusicCastMediaPlayer)
+ ]
- def media_play(self):
- """Send the media player the command for play/pause."""
- _LOGGER.debug("Play")
- self._recv.set_playback("play")
+ @property
+ def is_server(self) -> bool:
+ """Return whether the media player is the server/host of the group.
- def media_pause(self):
- """Send the media player the command for pause."""
- _LOGGER.debug("Pause")
- self._recv.set_playback("pause")
+ If the media player is not part of a group, False is returned.
+ """
+ return self.is_network_server or (
+ self._zone_id == DEFAULT_ZONE
+ and len(
+ [
+ entity
+ for entity in self.other_zones
+ if entity.source == ATTR_MAIN_SYNC
+ ]
+ )
+ > 0
+ )
- def media_stop(self):
- """Send the media player the stop command."""
- _LOGGER.debug("Stop")
- self._recv.set_playback("stop")
+ @property
+ def is_network_client(self) -> bool:
+ """Return True if the current entity is a network client and not just a main syncing entity."""
+ return (
+ self.coordinator.data.group_role == "client"
+ and self.coordinator.data.group_id != NULL_GROUP
+ and self.source == ATTR_MC_LINK
+ )
- def media_previous_track(self):
- """Send the media player the command for prev track."""
- _LOGGER.debug("Previous")
- self._recv.set_playback("previous")
+ @property
+ def is_client(self) -> bool:
+ """Return whether the media player is the client of a group.
- def media_next_track(self):
- """Send the media player the command for next track."""
- _LOGGER.debug("Next")
- self._recv.set_playback("next")
+ If the media player is not part of a group, False is returned.
+ """
+ return self.is_network_client or self.source == ATTR_MAIN_SYNC
- def mute_volume(self, mute):
- """Send mute command."""
- _LOGGER.debug("Mute volume: %s", mute)
- self._zone.set_mute(mute)
+ def get_all_mc_entities(self) -> list[MusicCastMediaPlayer]:
+ """Return all media player entities of the musiccast system."""
+ entities = []
+ for coordinator in self.hass.data[DOMAIN].values():
+ entities += [
+ entity
+ for entity in coordinator.entities
+ if isinstance(entity, MusicCastMediaPlayer)
+ ]
+ return entities
- def set_volume_level(self, volume):
- """Set volume level, range 0..1."""
- _LOGGER.debug("Volume level: %.2f / %d", volume, volume * self.volume_max)
- self._zone.set_volume(volume * self.volume_max)
+ def get_all_server_entities(self) -> list[MusicCastMediaPlayer]:
+ """Return all media player entities in the musiccast system, which are in server mode."""
+ entities = self.get_all_mc_entities()
+ return [entity for entity in entities if entity.is_server]
- def select_source(self, source):
- """Send the media player the command to select input source."""
- _LOGGER.debug("select_source: %s", source)
- self.status = STATE_UNKNOWN
- self._zone.set_input(source)
+ def get_distribution_num(self) -> int:
+ """Return the distribution_num (number of clients in the whole musiccast system)."""
+ return sum(
+ [
+ len(server.coordinator.data.group_client_list)
+ for server in self.get_all_server_entities()
+ ]
+ )
- def new_media_status(self, status):
- """Handle updates of the media status."""
- _LOGGER.debug("new media_status arrived")
- self.media_status = status
- self.media_status_received = dt_util.utcnow()
+ def is_part_of_group(self, group_server) -> bool:
+ """Return True if the given server is the server of self's group."""
+ return group_server != self and (
+ (
+ self.ip_address in group_server.coordinator.data.group_client_list
+ and self.coordinator.data.group_id
+ == group_server.coordinator.data.group_id
+ and self.ip_address != group_server.ip_address
+ and self.source == ATTR_MC_LINK
+ )
+ or (
+ self.ip_address == group_server.ip_address
+ and self.source == ATTR_MAIN_SYNC
+ )
+ )
+
+ @property
+ def group_server(self):
+ """Return the server of the own group if present, self else."""
+ for entity in self.get_all_server_entities():
+ if self.is_part_of_group(entity):
+ return entity
+ return self
+
+ @property
+ def group_members(self) -> list[str] | None:
+ """Return a list of entity_ids, which belong to the group of self."""
+ return [entity.entity_id for entity in self.musiccast_group]
+
+ @property
+ def musiccast_group(self) -> list[MusicCastMediaPlayer]:
+ """Return all media players of the current group, if the media player is server."""
+ if self.is_client:
+ # If we are a client we can still share group information, but we will take them from the server.
+ server = self.group_server
+ if server != self:
+ return server.musiccast_group
+
+ return [self]
+ if not self.is_server:
+ return [self]
+ entities = self.get_all_mc_entities()
+ clients = [entity for entity in entities if entity.is_part_of_group(self)]
+ return [self] + clients
+
+ @property
+ def musiccast_zone_entity(self) -> MusicCastMediaPlayer:
+ """Return the the entity of the zone, which is using MusicCast at the moment, if there is one, self else.
+
+ It is possible that multiple zones use MusicCast as client at the same time. In this case the first one is
+ returned.
+ """
+ for entity in self.other_zones:
+ if entity.is_network_server or entity.is_network_client:
+ return entity
+
+ return self
+
+ async def update_all_mc_entities(self):
+ """Update the whole musiccast system when group data change."""
+ for entity in self.get_all_mc_entities():
+ if entity.is_server:
+ await entity.async_check_client_list()
+ entity.async_write_ha_state()
+
+ # Services
+
+ async def async_join_players(self, group_members):
+ """Add all clients given in entities to the group of the server.
+
+ Creates a new group if necessary. Used for join service.
+ """
+ _LOGGER.info(
+ "%s wants to add the following entities %s",
+ self.entity_id,
+ str(group_members),
+ )
+
+ entities = [
+ entity
+ for entity in self.get_all_mc_entities()
+ if entity.entity_id in group_members
+ ]
+
+ if not self.is_server and self.musiccast_zone_entity.is_server:
+ # The MusicCast Distribution Module of this device is already in use. To use it as a server, we first
+ # have to unjoin and wait until the servers are updated.
+ await self.musiccast_zone_entity.async_server_close_group()
+ elif self.musiccast_zone_entity.is_client:
+ await self.async_client_leave_group(True)
+ # Use existing group id if we are server, generate a new one else.
+ group = (
+ self.coordinator.data.group_id
+ if self.is_server
+ else uuid.random_uuid_hex().upper()
+ )
+ # First let the clients join
+ for client in entities:
+ if client != self:
+ try:
+ await client.async_client_join(group, self)
+ except MusicCastGroupException:
+ _LOGGER.warning(
+ "%s is struggling to update its group data. Will retry perform the update",
+ client.entity_id,
+ )
+ await client.async_client_join(group, self)
+
+ await self.coordinator.musiccast.mc_server_group_extend(
+ self._zone_id,
+ [
+ entity.ip_address
+ for entity in entities
+ if entity.ip_address != self.ip_address
+ ],
+ group,
+ self.get_distribution_num(),
+ )
+ _LOGGER.debug(
+ "%s added the following entities %s", self.entity_id, str(entities)
+ )
+ _LOGGER.info(
+ "%s has now the following musiccast group %s",
+ self.entity_id,
+ str(self.musiccast_group),
+ )
+
+ await self.update_all_mc_entities()
+
+ async def async_unjoin_player(self):
+ """Leave the group.
+
+ Stops the distribution if device is server. Used for unjoin service.
+ """
+ _LOGGER.debug("%s called service unjoin", self.entity_id)
+ if self.is_server:
+ await self.async_server_close_group()
+
+ else:
+ await self.async_client_leave_group()
+
+ await self.update_all_mc_entities()
+
+ # Internal client functions
+
+ async def async_client_join(self, group_id, server):
+ """Let the client join a group.
+
+ If this client is a server, the server will stop distributing. If the client is part of a different group,
+ it will leave that group first.
+ """
+ # If we should join the group, which is served by the main zone, we can simply select main_sync as input.
+ _LOGGER.debug("%s called service client join", self.entity_id)
+ if self.state == STATE_OFF:
+ await self.async_turn_on()
+ if self.ip_address == server.ip_address:
+ if server.zone == DEFAULT_ZONE:
+ await self.async_select_source(ATTR_MAIN_SYNC)
+ server.async_write_ha_state()
+ return
+
+ # It is not possible to join a group hosted by zone2 from main zone.
+ raise Exception("Can not join a zone other than main of the same device.")
+
+ if self.musiccast_zone_entity.is_server:
+ # If one of the zones of the device is a server, we need to unjoin first.
+ _LOGGER.info(
+ "%s is a server of a group and has to stop distribution "
+ "to use MusicCast for %s",
+ self.musiccast_zone_entity.entity_id,
+ self.entity_id,
+ )
+ await self.musiccast_zone_entity.async_server_close_group()
+
+ elif self.is_client:
+ if self.coordinator.data.group_id == server.coordinator.data.group_id:
+ _LOGGER.warning("%s is already part of the group", self.entity_id)
+ return
+
+ _LOGGER.info(
+ "%s is client in a different group, will unjoin first",
+ self.entity_id,
+ )
+ await self.async_client_leave_group()
+
+ elif (
+ self.ip_address in server.coordinator.data.group_client_list
+ and self.coordinator.data.group_id == server.coordinator.data.group_id
+ and self.coordinator.data.group_role == "client"
+ ):
+ # The device is already part of this group (e.g. main zone is also a client of this group).
+ # Just select mc_link as source
+ await self.async_select_source(ATTR_MC_LINK)
+ # As the musiccast group has changed, we need to trigger the servers ha state.
+ # In other cases this happens due to the callback after the dist updated message.
+ server.async_write_ha_state()
+ return
+
+ _LOGGER.debug("%s will now join as a client", self.entity_id)
+ await self.coordinator.musiccast.mc_client_join(
+ server.ip_address, group_id, self._zone_id
+ )
+
+ # Ensure that mc link is selected. If main sync was selected previously, it's possible that this does not
+ # happen automatically
+ await self.async_select_source(ATTR_MC_LINK)
+
+ async def async_client_leave_group(self, force=False):
+ """Make self leave the group.
+
+ Should only be called for clients.
+ """
+ _LOGGER.debug("%s client leave called", self.entity_id)
+ if not force and (
+ self.source == ATTR_MAIN_SYNC
+ or len(
+ [entity for entity in self.other_zones if entity.source == ATTR_MC_LINK]
+ )
+ > 0
+ ):
+ # If we are only syncing to main or another zone is also using the musiccast module as client, don't
+ # kill the client session, just select a dummy source.
+ save_inputs = self.coordinator.musiccast.get_save_inputs(self._zone_id)
+ if len(save_inputs):
+ await self.async_select_source(save_inputs[0])
+ # Then turn off the zone
+ await self.async_turn_off()
+ else:
+ servers = [
+ server
+ for server in self.get_all_server_entities()
+ if server.coordinator.data.group_id == self.coordinator.data.group_id
+ ]
+ await self.coordinator.musiccast.mc_client_unjoin()
+ if len(servers):
+ await servers[0].coordinator.musiccast.mc_server_group_reduce(
+ servers[0].zone_id, [self.ip_address], self.get_distribution_num()
+ )
+
+ for server in self.get_all_server_entities():
+ await server.async_check_client_list()
+
+ # Internal server functions
+
+ async def async_server_close_group(self):
+ """Close group of self.
+
+ Should only be called for servers.
+ """
+ _LOGGER.info("%s closes his group", self.entity_id)
+ for client in self.musiccast_group:
+ if client != self:
+ await client.async_client_leave_group()
+ await self.coordinator.musiccast.mc_server_group_close()
+
+ async def async_check_client_list(self):
+ """Let the server check if all its clients are still part of his group."""
+ _LOGGER.debug("%s updates his group members", self.entity_id)
+ client_ips_for_removal = []
+ for expected_client_ip in self.coordinator.data.group_client_list:
+ if expected_client_ip not in [
+ entity.ip_address for entity in self.musiccast_group
+ ]:
+ # The client is no longer part of the group. Prepare removal.
+ client_ips_for_removal.append(expected_client_ip)
+
+ if len(client_ips_for_removal) > 0:
+ _LOGGER.info(
+ "%s says good bye to the following members %s",
+ self.entity_id,
+ str(client_ips_for_removal),
+ )
+ await self.coordinator.musiccast.mc_server_group_reduce(
+ self._zone_id, client_ips_for_removal, self.get_distribution_num()
+ )
+ if len(self.musiccast_group) < 2:
+ # The group is empty, stop distribution.
+ await self.async_server_close_group()
+
+ self.async_write_ha_state()
diff --git a/homeassistant/components/yamaha_musiccast/strings.json b/homeassistant/components/yamaha_musiccast/strings.json
new file mode 100644
index 00000000000..e2261882222
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/strings.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "flow_title": "MusicCast: {name}",
+ "step": {
+ "user": {
+ "description": "Set up MusicCast to integrate with Home Assistant.",
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]"
+ }
+ },
+ "confirm": {
+ "description": "[%key:common::config_flow::description::confirm_setup%]"
+ }
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "yxc_control_url_missing": "The control URL is not given in the ssdp description."
+ },
+ "error": {
+ "no_musiccast_device": "This device seems to be no MusicCast Device."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yamaha_musiccast/translations/ca.json b/homeassistant/components/yamaha_musiccast/translations/ca.json
new file mode 100644
index 00000000000..32cd231c963
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/ca.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "yxc_control_url_missing": "No s'ha proporcionat l'URL de control en la descripci\u00f3 ssdp."
+ },
+ "error": {
+ "no_musiccast_device": "Aquest dispositiu sembla no ser un dispositiu MusicCast."
+ },
+ "flow_title": "MusicCast: {name}",
+ "step": {
+ "confirm": {
+ "description": "Vols comen\u00e7ar la configuraci\u00f3?"
+ },
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3"
+ },
+ "description": "Configura MusicCast per a integrar-lo amb Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yamaha_musiccast/translations/de.json b/homeassistant/components/yamaha_musiccast/translations/de.json
new file mode 100644
index 00000000000..49e66419bdf
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/de.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "[%key::common::config_flow::abort::already_configured_device%]",
+ "yxc_control_url_missing": "Die Steuer-URL ist in der ssdp-Beschreibung nicht angegeben."
+ },
+ "error": {
+ "no_musiccast_device": "Dieses Ger\u00e4t scheint kein MusicCast-Ger\u00e4t zu sein."
+ },
+ "flow_title": "MusicCast: {name}",
+ "step": {
+ "confirm": {
+ "description": "[%key::common::config_flow::description::confirm_setup%]"
+ },
+ "user": {
+ "data": {
+ "host": "[%key::common::config_flow::data::host%]"
+ },
+ "description": "Einrichten von MusicCast zur Integration mit Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yamaha_musiccast/translations/en.json b/homeassistant/components/yamaha_musiccast/translations/en.json
new file mode 100644
index 00000000000..4c7f3b45f0b
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/en.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured",
+ "yxc_control_url_missing": "The control URL is not given in the ssdp description."
+ },
+ "error": {
+ "no_musiccast_device": "This device seems to be no MusicCast Device."
+ },
+ "flow_title": "MusicCast: {name}",
+ "step": {
+ "confirm": {
+ "description": "Do you want to start set up?"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Set up MusicCast to integrate with Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yamaha_musiccast/translations/es.json b/homeassistant/components/yamaha_musiccast/translations/es.json
new file mode 100644
index 00000000000..c63baa7b576
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/es.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "yxc_control_url_missing": "La URL de control no se proporciona en la descripci\u00f3n del ssdp."
+ },
+ "error": {
+ "no_musiccast_device": "Este dispositivo no parece ser un dispositivo MusicCast."
+ },
+ "flow_title": "MusicCast: {name}",
+ "step": {
+ "user": {
+ "description": "Configura MusicCast para integrarse con Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yamaha_musiccast/translations/et.json b/homeassistant/components/yamaha_musiccast/translations/et.json
new file mode 100644
index 00000000000..8283298f11b
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/et.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "yxc_control_url_missing": "Juhtelemendi URL-i pole ssdp kirjelduses esitatud."
+ },
+ "error": {
+ "no_musiccast_device": "Tundub, et leitud seade pole MusicCasti seade."
+ },
+ "flow_title": "MusicCast: {name}",
+ "step": {
+ "confirm": {
+ "description": "Kas soovid alustada seadistamist?"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Seadista MusicCast'i sidumine Home Assistantiga."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yamaha_musiccast/translations/he.json b/homeassistant/components/yamaha_musiccast/translations/he.json
new file mode 100644
index 00000000000..8c8484a1160
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/he.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
+ },
+ "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/yamaha_musiccast/translations/hu.json b/homeassistant/components/yamaha_musiccast/translations/hu.json
new file mode 100644
index 00000000000..0f973ce6dcc
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/hu.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "step": {
+ "confirm": {
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?"
+ },
+ "user": {
+ "data": {
+ "host": "Hoszt"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yamaha_musiccast/translations/it.json b/homeassistant/components/yamaha_musiccast/translations/it.json
new file mode 100644
index 00000000000..09a9e698ab5
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/it.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "yxc_control_url_missing": "L'URL di controllo non \u00e8 fornito nella descrizione ssdp."
+ },
+ "error": {
+ "no_musiccast_device": "Questo dispositivo sembra non essere un dispositivo MusicCast."
+ },
+ "flow_title": "MusicCast: {name}",
+ "step": {
+ "confirm": {
+ "description": "Vuoi iniziare la configurazione?"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Configura MusicCast per l'integrazione con Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yamaha_musiccast/translations/nl.json b/homeassistant/components/yamaha_musiccast/translations/nl.json
new file mode 100644
index 00000000000..e1e31149c06
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/nl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "yxc_control_url_missing": "De controle-URL wordt niet gegeven in de ssdp-beschrijving."
+ },
+ "error": {
+ "no_musiccast_device": "Dit apparaat lijkt geen MusicCast-apparaat te zijn."
+ },
+ "flow_title": "MusicCast: {name}",
+ "step": {
+ "confirm": {
+ "description": "Wil je beginnen met instellen?"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Stel MusicCast in om te integreren met Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yamaha_musiccast/translations/no.json b/homeassistant/components/yamaha_musiccast/translations/no.json
new file mode 100644
index 00000000000..5381cdc5d67
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/no.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "yxc_control_url_missing": "Kontroll-URL-en er ikke gitt i ssdp-beskrivelsen."
+ },
+ "error": {
+ "no_musiccast_device": "Denne enheten ser ut til \u00e5 v\u00e6re ingen MusicCast-enhet."
+ },
+ "flow_title": "MusicCast: {name}",
+ "step": {
+ "confirm": {
+ "description": "Vil du starte oppsettet?"
+ },
+ "user": {
+ "data": {
+ "host": "Vert"
+ },
+ "description": "Sett opp MusicCast for \u00e5 integrere med Home Assistant."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yamaha_musiccast/translations/pl.json b/homeassistant/components/yamaha_musiccast/translations/pl.json
new file mode 100644
index 00000000000..d22126557d0
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/pl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "yxc_control_url_missing": "Kontrolny adres URL nie jest podany w opisie ssdp."
+ },
+ "error": {
+ "no_musiccast_device": "Wygl\u0105da na to, \u017ce to urz\u0105dzenie nie jest urz\u0105dzeniem MusicCast."
+ },
+ "flow_title": "MusicCast: {name}",
+ "step": {
+ "confirm": {
+ "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?"
+ },
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP"
+ },
+ "description": "Skonfiguruj MusicCast, aby zintegrowa\u0107 go z Home Assistantem."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yamaha_musiccast/translations/ru.json b/homeassistant/components/yamaha_musiccast/translations/ru.json
new file mode 100644
index 00000000000..161a97ad15d
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/ru.json
@@ -0,0 +1,23 @@
+{
+ "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.",
+ "yxc_control_url_missing": "URL-\u0430\u0434\u0440\u0435\u0441 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 ssdp."
+ },
+ "error": {
+ "no_musiccast_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 MusicCast."
+ },
+ "flow_title": "MusicCast: {name}",
+ "step": {
+ "confirm": {
+ "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 MusicCast."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json b/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json
new file mode 100644
index 00000000000..45664e0b815
--- /dev/null
+++ b/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "yxc_control_url_missing": "SSDP \u63cf\u8ff0\u4e2d\u672a\u5305\u542b\u63a7\u5236 URL\u3002"
+ },
+ "error": {
+ "no_musiccast_device": "\u6b64\u88dd\u7f6e\u4f3c\u4e4e\u4e0d\u662f MusicCast \u88dd\u7f6e\u3002"
+ },
+ "flow_title": "MusicCast\uff1a{name}",
+ "step": {
+ "confirm": {
+ "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f"
+ },
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef"
+ },
+ "description": "\u8a2d\u5b9a MusicCast \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py
index b18f18b6fa4..2cb754ce6a7 100644
--- a/homeassistant/components/yeelight/__init__.py
+++ b/homeassistant/components/yeelight/__init__.py
@@ -164,16 +164,11 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
# Import manually configured devices
for host, device_config in config.get(DOMAIN, {}).get(CONF_DEVICES, {}).items():
_LOGGER.debug("Importing configured %s", host)
- entry_config = {
- CONF_HOST: host,
- **device_config,
- }
+ entry_config = {CONF_HOST: host, **device_config}
hass.async_create_task(
hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_IMPORT},
- data=entry_config,
- ),
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config
+ )
)
return True
@@ -203,9 +198,7 @@ async def _async_initialize(
entry.async_on_unload(
async_dispatcher_connect(
- hass,
- DEVICE_INITIALIZED.format(host),
- _async_load_platforms,
+ hass, DEVICE_INITIALIZED.format(host), _async_load_platforms
)
)
@@ -224,10 +217,7 @@ def _async_populate_entry_options(hass: HomeAssistant, entry: ConfigEntry) -> No
hass.config_entries.async_update_entry(
entry,
- data={
- CONF_HOST: entry.data.get(CONF_HOST),
- CONF_ID: entry.data.get(CONF_ID),
- },
+ data={CONF_HOST: entry.data.get(CONF_HOST), CONF_ID: entry.data.get(CONF_ID)},
options={
CONF_NAME: entry.data.get(CONF_NAME, ""),
CONF_MODEL: entry.data.get(CONF_MODEL, ""),
@@ -271,7 +261,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
data_config_entries = hass.data[DOMAIN][DATA_CONFIG_ENTRIES]
entry_data = data_config_entries[entry.entry_id]
@@ -613,9 +603,7 @@ class YeelightEntity(Entity):
async def _async_get_device(
- hass: HomeAssistant,
- host: str,
- entry: ConfigEntry,
+ hass: HomeAssistant, host: str, entry: ConfigEntry
) -> YeelightDevice:
# Get model from config and capabilities
model = entry.options.get(CONF_MODEL)
diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py
index 0b0fe0d96c1..a66571cae93 100644
--- a/homeassistant/components/yeelight/config_flow.py
+++ b/homeassistant/components/yeelight/config_flow.py
@@ -83,10 +83,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
)
self._set_confirm_only()
- placeholders = {
- "model": self._discovered_model,
- "host": self._discovered_ip,
- }
+ placeholders = {"model": self._discovered_model, "host": self._discovered_ip}
self.context["title_placeholders"] = placeholders
return self.async_show_form(
step_id="discovery_confirm", description_placeholders=placeholders
@@ -105,8 +102,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
else:
self._abort_if_unique_id_configured()
return self.async_create_entry(
- title=f"{model} {self.unique_id}",
- data=user_input,
+ title=f"{model} {self.unique_id}", data=user_input
)
user_input = user_input or {}
@@ -126,8 +122,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
- title=_async_unique_name(capabilities),
- data={CONF_ID: unique_id},
+ title=_async_unique_name(capabilities), data={CONF_ID: unique_id}
)
configured_devices = {
@@ -223,19 +218,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
{
vol.Optional(CONF_MODEL, default=options[CONF_MODEL]): str,
vol.Required(
- CONF_TRANSITION,
- default=options[CONF_TRANSITION],
+ CONF_TRANSITION, default=options[CONF_TRANSITION]
): cv.positive_int,
vol.Required(
CONF_MODE_MUSIC, default=options[CONF_MODE_MUSIC]
): bool,
vol.Required(
- CONF_SAVE_ON_CHANGE,
- default=options[CONF_SAVE_ON_CHANGE],
+ CONF_SAVE_ON_CHANGE, default=options[CONF_SAVE_ON_CHANGE]
): bool,
vol.Required(
- CONF_NIGHTLIGHT_SWITCH,
- default=options[CONF_NIGHTLIGHT_SWITCH],
+ CONF_NIGHTLIGHT_SWITCH, default=options[CONF_NIGHTLIGHT_SWITCH]
): bool,
}
),
diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py
index 0782ab94c61..8d0a3b0ffd4 100644
--- a/homeassistant/components/yeelight/light.py
+++ b/homeassistant/components/yeelight/light.py
@@ -6,15 +6,7 @@ import logging
import voluptuous as vol
import yeelight
-from yeelight import (
- Bulb,
- BulbException,
- Flow,
- RGBTransition,
- SleepTransition,
- flows,
- transitions as yee_transitions,
-)
+from yeelight import Bulb, BulbException, Flow, RGBTransition, SleepTransition, flows
from yeelight.enums import BulbType, LightType, PowerMode, SceneClass
from homeassistant.components.light import (
@@ -26,11 +18,14 @@ from homeassistant.components.light import (
ATTR_KELVIN,
ATTR_RGB_COLOR,
ATTR_TRANSITION,
+ COLOR_MODE_BRIGHTNESS,
+ COLOR_MODE_COLOR_TEMP,
+ COLOR_MODE_HS,
+ COLOR_MODE_ONOFF,
+ COLOR_MODE_RGB,
+ COLOR_MODE_UNKNOWN,
FLASH_LONG,
FLASH_SHORT,
- SUPPORT_BRIGHTNESS,
- SUPPORT_COLOR,
- SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
@@ -70,13 +65,7 @@ from . import (
_LOGGER = logging.getLogger(__name__)
-SUPPORT_YEELIGHT = (
- SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_EFFECT
-)
-
-SUPPORT_YEELIGHT_WHITE_TEMP = SUPPORT_YEELIGHT | SUPPORT_COLOR_TEMP
-
-SUPPORT_YEELIGHT_RGB = SUPPORT_YEELIGHT_WHITE_TEMP | SUPPORT_COLOR
+SUPPORT_YEELIGHT = SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_EFFECT
ATTR_MINUTES = "minutes"
@@ -180,9 +169,7 @@ SERVICE_SCHEMA_SET_MODE = {
vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode])
}
-SERVICE_SCHEMA_SET_MUSIC_MODE = {
- vol.Required(ATTR_MODE_MUSIC): cv.boolean,
-}
+SERVICE_SCHEMA_SET_MUSIC_MODE = {vol.Required(ATTR_MODE_MUSIC): cv.boolean}
SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA
@@ -283,7 +270,7 @@ async def async_setup_entry(
elif device_type == BulbType.Color:
if nl_switch_light and device.is_nightlight_supported:
_lights_setup_helper(YeelightColorLightWithNightlightSwitch)
- _lights_setup_helper(YeelightNightLightModeWithWithoutBrightnessControl)
+ _lights_setup_helper(YeelightNightLightModeWithoutBrightnessControl)
else:
_lights_setup_helper(YeelightColorLightWithoutNightlightSwitch)
elif device_type == BulbType.WhiteTemp:
@@ -358,11 +345,7 @@ def _async_setup_services(hass: HomeAssistant):
transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]),
)
await hass.async_add_executor_job(
- partial(
- entity.set_scene,
- SceneClass.CF,
- flow,
- )
+ partial(entity.set_scene, SceneClass.CF, flow)
)
async def _async_set_auto_delay_off_scene(entity, service_call):
@@ -378,24 +361,16 @@ def _async_setup_services(hass: HomeAssistant):
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
- SERVICE_SET_MODE,
- SERVICE_SCHEMA_SET_MODE,
- "set_mode",
+ SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "set_mode"
)
platform.async_register_entity_service(
- SERVICE_START_FLOW,
- SERVICE_SCHEMA_START_FLOW,
- _async_start_flow,
+ SERVICE_START_FLOW, SERVICE_SCHEMA_START_FLOW, _async_start_flow
)
platform.async_register_entity_service(
- SERVICE_SET_COLOR_SCENE,
- SERVICE_SCHEMA_SET_COLOR_SCENE,
- _async_set_color_scene,
+ SERVICE_SET_COLOR_SCENE, SERVICE_SCHEMA_SET_COLOR_SCENE, _async_set_color_scene
)
platform.async_register_entity_service(
- SERVICE_SET_HSV_SCENE,
- SERVICE_SCHEMA_SET_HSV_SCENE,
- _async_set_hsv_scene,
+ SERVICE_SET_HSV_SCENE, SERVICE_SCHEMA_SET_HSV_SCENE, _async_set_hsv_scene
)
platform.async_register_entity_service(
SERVICE_SET_COLOR_TEMP_SCENE,
@@ -413,15 +388,16 @@ def _async_setup_services(hass: HomeAssistant):
_async_set_auto_delay_off_scene,
)
platform.async_register_entity_service(
- SERVICE_SET_MUSIC_MODE,
- SERVICE_SCHEMA_SET_MUSIC_MODE,
- "set_music_mode",
+ SERVICE_SET_MUSIC_MODE, SERVICE_SCHEMA_SET_MUSIC_MODE, "set_music_mode"
)
class YeelightGenericLight(YeelightEntity, LightEntity):
"""Representation of a Yeelight generic light."""
+ _attr_color_mode = COLOR_MODE_BRIGHTNESS
+ _attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS}
+
def __init__(self, device, entry, custom_effects=None):
"""Initialize the Yeelight light."""
super().__init__(device, entry)
@@ -430,6 +406,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
self._color_temp = None
self._hs = None
+ self._rgb = None
self._effect = None
model_specs = self._bulb.get_model_specs()
@@ -527,6 +504,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
"""Return the color property."""
return self._hs
+ @property
+ def rgb_color(self) -> tuple:
+ """Return the color property."""
+ return self._rgb
+
@property
def effect(self):
"""Return the current effect."""
@@ -582,32 +564,30 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
def update(self):
"""Update light properties."""
self._hs = self._get_hs_from_properties()
+ self._rgb = self._get_rgb_from_properties()
if not self.device.is_color_flow_enabled:
self._effect = None
def _get_hs_from_properties(self):
- rgb = self._get_property("rgb")
- color_mode = self._get_property("color_mode")
-
- if not rgb or not color_mode:
+ hue = self._get_property("hue")
+ sat = self._get_property("sat")
+ if hue is None or sat is None:
return None
- color_mode = int(color_mode)
- if color_mode == 2: # color temperature
- temp_in_k = mired_to_kelvin(self.color_temp)
- return color_util.color_temperature_to_hs(temp_in_k)
- if color_mode == 3: # hsv
- hue = int(self._get_property("hue"))
- sat = int(self._get_property("sat"))
+ return (int(hue), int(sat))
- return (hue / 360 * 65536, sat / 100 * 255)
+ def _get_rgb_from_properties(self):
+ rgb = self._get_property("rgb")
+
+ if rgb is None:
+ return None
rgb = int(rgb)
blue = rgb & 0xFF
green = (rgb >> 8) & 0xFF
red = (rgb >> 16) & 0xFF
- return color_util.color_RGB_to_hs(red, green, blue)
+ return (red, green, blue)
def set_music_mode(self, music_mode) -> None:
"""Set the music mode on or off."""
@@ -630,10 +610,19 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
brightness / 255 * 100, duration=duration, light_type=self.light_type
)
+ @_cmd
+ def set_hs(self, hs_color, duration) -> None:
+ """Set bulb's color."""
+ if hs_color and COLOR_MODE_HS in self.supported_color_modes:
+ _LOGGER.debug("Setting HS: %s", hs_color)
+ self._bulb.set_hsv(
+ hs_color[0], hs_color[1], duration=duration, light_type=self.light_type
+ )
+
@_cmd
def set_rgb(self, rgb, duration) -> None:
"""Set bulb's color."""
- if rgb and self.supported_features & SUPPORT_COLOR:
+ if rgb and COLOR_MODE_RGB in self.supported_color_modes:
_LOGGER.debug("Setting RGB: %s", rgb)
self._bulb.set_rgb(
rgb[0], rgb[1], rgb[2], duration=duration, light_type=self.light_type
@@ -642,7 +631,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
@_cmd
def set_colortemp(self, colortemp, duration) -> None:
"""Set bulb's color temperature."""
- if colortemp and self.supported_features & SUPPORT_COLOR_TEMP:
+ if colortemp and COLOR_MODE_COLOR_TEMP in self.supported_color_modes:
temp_in_k = mired_to_kelvin(colortemp)
_LOGGER.debug("Setting color temp: %s K", temp_in_k)
@@ -707,11 +696,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
elif effect == EFFECT_FAST_RANDOM_LOOP:
flow = flows.random_loop(duration=250)
elif effect == EFFECT_WHATSAPP:
- flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102))
+ flow = flows.pulse(37, 211, 102, count=2)
elif effect == EFFECT_FACEBOOK:
- flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152))
+ flow = flows.pulse(59, 89, 152, count=2)
elif effect == EFFECT_TWITTER:
- flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237))
+ flow = flows.pulse(0, 172, 237, count=2)
else:
return
@@ -726,7 +715,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
brightness = kwargs.get(ATTR_BRIGHTNESS)
colortemp = kwargs.get(ATTR_COLOR_TEMP)
hs_color = kwargs.get(ATTR_HS_COLOR)
- rgb = color_util.color_hs_to_RGB(*hs_color) if hs_color else None
+ rgb = kwargs.get(ATTR_RGB_COLOR)
flash = kwargs.get(ATTR_FLASH)
effect = kwargs.get(ATTR_EFFECT)
@@ -750,6 +739,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
try:
# values checked for none in methods
+ self.set_hs(hs_color, duration)
self.set_rgb(rgb, duration)
self.set_colortemp(colortemp, duration)
self.set_brightness(brightness, duration)
@@ -810,13 +800,23 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
_LOGGER.error("Unable to set scene: %s", ex)
-class YeelightColorLightSupport:
+class YeelightColorLightSupport(YeelightGenericLight):
"""Representation of a Color Yeelight light support."""
+ _attr_supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, COLOR_MODE_RGB}
+
@property
- def supported_features(self) -> int:
- """Flag supported features."""
- return SUPPORT_YEELIGHT_RGB
+ def color_mode(self):
+ """Return the color mode."""
+ color_mode = int(self._get_property("color_mode"))
+ if color_mode == 1: # RGB
+ return COLOR_MODE_RGB
+ if color_mode == 2: # color temperature
+ return COLOR_MODE_COLOR_TEMP
+ if color_mode == 3: # hsv
+ return COLOR_MODE_HS
+ _LOGGER.debug("Light reported unknown color mode: %s", color_mode)
+ return COLOR_MODE_UNKNOWN
@property
def _predefined_effects(self):
@@ -824,12 +824,10 @@ class YeelightColorLightSupport:
class YeelightWhiteTempLightSupport:
- """Representation of a Color Yeelight light."""
+ """Representation of a White temp Yeelight light."""
- @property
- def supported_features(self) -> int:
- """Flag supported features."""
- return SUPPORT_YEELIGHT_WHITE_TEMP
+ _attr_color_mode = COLOR_MODE_COLOR_TEMP
+ _attr_supported_color_modes = {COLOR_MODE_COLOR_TEMP}
@property
def _predefined_effects(self):
@@ -937,12 +935,15 @@ class YeelightNightLightModeWithAmbientSupport(YeelightNightLightMode):
return "main_power"
-class YeelightNightLightModeWithWithoutBrightnessControl(YeelightNightLightMode):
+class YeelightNightLightModeWithoutBrightnessControl(YeelightNightLightMode):
"""Representation of a Yeelight, when in nightlight mode.
It represents case when nightlight mode brightness control is not supported.
"""
+ _attr_color_mode = COLOR_MODE_ONOFF
+ _attr_supported_color_modes = {COLOR_MODE_ONOFF}
+
@property
def supported_features(self):
"""Flag no supported features."""
diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json
index 1a8f9a463b7..0c9e6d2a154 100644
--- a/homeassistant/components/yeelight/translations/de.json
+++ b/homeassistant/components/yeelight/translations/de.json
@@ -7,6 +7,7 @@
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
},
+ "flow_title": "{model} {host}",
"step": {
"discovery_confirm": {
"description": "M\u00f6chten Sie {model} ({host}) einrichten?"
diff --git a/homeassistant/components/yeelight/translations/he.json b/homeassistant/components/yeelight/translations/he.json
new file mode 100644
index 00000000000..adfd4d904ce
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/he.json
@@ -0,0 +1,42 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
+ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{model} {host}",
+ "step": {
+ "discovery_confirm": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?"
+ },
+ "pick_device": {
+ "data": {
+ "device": "\u05d4\u05ea\u05e7\u05df"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7"
+ },
+ "description": "\u05d0\u05dd \u05ea\u05e9\u05d0\u05d9\u05e8 \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05e8\u05d9\u05e7, \u05d4\u05d2\u05d9\u05dc\u05d5\u05d9 \u05d9\u05e9\u05de\u05e9 \u05dc\u05de\u05e6\u05d9\u05d0\u05ea \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "model": "\u05d3\u05d2\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)",
+ "nightlight_switch": "\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05de\u05ea\u05d2 \u05ea\u05d0\u05d5\u05e8\u05ea \u05dc\u05d9\u05dc\u05d4",
+ "save_on_change": "\u05e9\u05de\u05d5\u05e8 \u05e1\u05d8\u05d8\u05d5\u05e1 \u05d1\u05e9\u05d9\u05e0\u05d5\u05d9",
+ "transition": "\u05d6\u05de\u05df \u05de\u05e2\u05d1\u05e8 (\u05d0\u05dc\u05e4\u05d9\u05d5\u05ea \u05e9\u05e0\u05d9\u05d4)",
+ "use_music_mode": "\u05d4\u05e4\u05e2\u05dc\u05ea \u05de\u05e6\u05d1 \u05de\u05d5\u05e1\u05d9\u05e7\u05d4"
+ },
+ "description": "\u05d0\u05dd \u05ea\u05e9\u05d0\u05d9\u05e8 \u05d0\u05ea \u05d4\u05d3\u05d2\u05dd \u05e8\u05d9\u05e7, \u05d4\u05d5\u05d0 \u05d9\u05d6\u05d5\u05d4\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yeelight/translations/hu.json b/homeassistant/components/yeelight/translations/hu.json
index ac463142359..cdb0839dd5a 100644
--- a/homeassistant/components/yeelight/translations/hu.json
+++ b/homeassistant/components/yeelight/translations/hu.json
@@ -7,6 +7,7 @@
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
+ "flow_title": "{model} {host}",
"step": {
"pick_device": {
"data": {
diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py
index 64d631e2850..d8d664b63c5 100644
--- a/homeassistant/components/zeroconf/__init__.py
+++ b/homeassistant/components/zeroconf/__init__.py
@@ -15,10 +15,9 @@ from zeroconf import (
InterfaceChoice,
IPVersion,
NonUniqueNameException,
- ServiceInfo,
ServiceStateChange,
- Zeroconf,
)
+from zeroconf.asyncio import AsyncServiceInfo
from homeassistant import config_entries, util
from homeassistant.components import network
@@ -35,7 +34,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass
-from .models import HaAsyncZeroconf, HaServiceBrowser, HaZeroconf
+from .models import HaAsyncServiceBrowser, HaAsyncZeroconf, HaZeroconf
from .usage import install_multiple_zeroconf_catcher
_LOGGER = logging.getLogger(__name__)
@@ -70,6 +69,7 @@ CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
cv.deprecated(CONF_DEFAULT_INTERFACE),
+ cv.deprecated(CONF_IPV6),
vol.Schema(
{
vol.Optional(CONF_DEFAULT_INTERFACE): cv.boolean,
@@ -119,16 +119,16 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
- aio_zc = HaAsyncZeroconf(**zcargs)
- zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
+ zeroconf = HaZeroconf(**zcargs)
+ aio_zc = HaAsyncZeroconf(zc=zeroconf)
install_multiple_zeroconf_catcher(zeroconf)
- def _stop_zeroconf(_event: Event) -> None:
+ async def _async_stop_zeroconf(_event: Event) -> None:
"""Stop Zeroconf."""
- zeroconf.ha_close()
+ await aio_zc.ha_async_close()
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_zeroconf)
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_zeroconf)
hass.data[DOMAIN] = aio_zc
return aio_zc
@@ -143,7 +143,6 @@ def _async_use_default_interface(adapters: list[Adapter]) -> bool:
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up Zeroconf and make Home Assistant discoverable."""
- zc_config = config.get(DOMAIN, {})
zc_args: dict = {}
adapters = await network.async_get_adapters(hass)
@@ -158,16 +157,18 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
interfaces.append(ipv4s[0]["address"])
elif ipv6s := adapter["ipv6"]:
interfaces.append(ipv6s[0]["scope_id"])
- if not zc_config.get(CONF_IPV6, DEFAULT_IPV6):
+
+ ipv6 = True
+ if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
+ ipv6 = False
zc_args["ip_version"] = IPVersion.V4Only
aio_zc = await _async_get_instance(hass, **zc_args)
- zeroconf = aio_zc.zeroconf
-
+ zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
zeroconf_types, homekit_models = await asyncio.gather(
async_get_zeroconf(hass), async_get_homekit(hass)
)
- discovery = ZeroconfDiscovery(hass, zeroconf, zeroconf_types, homekit_models)
+ discovery = ZeroconfDiscovery(hass, zeroconf, zeroconf_types, homekit_models, ipv6)
await discovery.async_setup()
async def _async_zeroconf_hass_start(_event: Event) -> None:
@@ -230,7 +231,7 @@ async def _async_register_hass_zc_service(
_suppress_invalid_properties(params)
- info = ServiceInfo(
+ info = AsyncServiceInfo(
ZEROCONF_TYPE,
name=f"{valid_location_name}.{ZEROCONF_TYPE}",
server=f"{uuid}.local.",
@@ -268,10 +269,10 @@ class FlowDispatcher:
self.hass.async_create_task(self._init_flow(flow))
self.pending_flows = []
- def create(self, flow: ZeroconfFlow) -> None:
+ def async_create(self, flow: ZeroconfFlow) -> None:
"""Create and add or queue a flow."""
if self.started:
- self.hass.create_task(self._init_flow(flow))
+ self.hass.async_create_task(self._init_flow(flow))
else:
self.pending_flows.append(flow)
@@ -288,18 +289,20 @@ class ZeroconfDiscovery:
def __init__(
self,
hass: HomeAssistant,
- zeroconf: Zeroconf,
+ zeroconf: HaZeroconf,
zeroconf_types: dict[str, list[dict[str, str]]],
homekit_models: dict[str, str],
+ ipv6: bool,
) -> None:
"""Init discovery."""
self.hass = hass
self.zeroconf = zeroconf
self.zeroconf_types = zeroconf_types
self.homekit_models = homekit_models
+ self.ipv6 = ipv6
self.flow_dispatcher: FlowDispatcher | None = None
- self.service_browser: HaServiceBrowser | None = None
+ self.async_service_browser: HaAsyncServiceBrowser | None = None
async def async_setup(self) -> None:
"""Start discovery."""
@@ -311,15 +314,15 @@ class ZeroconfDiscovery:
for hk_type in (ZEROCONF_TYPE, *HOMEKIT_TYPES):
if hk_type not in self.zeroconf_types:
types.append(hk_type)
- _LOGGER.debug("Starting Zeroconf browser")
- self.service_browser = HaServiceBrowser(
- self.zeroconf, types, handlers=[self.service_update]
+ _LOGGER.debug("Starting Zeroconf browser for: %s", types)
+ self.async_service_browser = HaAsyncServiceBrowser(
+ self.ipv6, self.zeroconf, types, handlers=[self.async_service_update]
)
async def async_stop(self) -> None:
"""Cancel the service browser and stop processing the queue."""
- if self.service_browser:
- await self.hass.async_add_executor_job(self.service_browser.cancel)
+ if self.async_service_browser:
+ await self.async_service_browser.async_cancel()
@callback
def async_start(self) -> None:
@@ -327,21 +330,35 @@ class ZeroconfDiscovery:
assert self.flow_dispatcher is not None
self.flow_dispatcher.async_start()
- def service_update(
+ @callback
+ def async_service_update(
self,
- zeroconf: Zeroconf,
+ zeroconf: HaZeroconf,
service_type: str,
name: str,
state_change: ServiceStateChange,
) -> None:
"""Service state changed."""
+ _LOGGER.debug(
+ "service_update: type=%s name=%s state_change=%s",
+ service_type,
+ name,
+ state_change,
+ )
+
if state_change == ServiceStateChange.Removed:
return
- service_info = ServiceInfo(service_type, name)
- service_info.load_from_cache(zeroconf)
+ asyncio.create_task(self._process_service_update(zeroconf, service_type, name))
- info = info_from_service(service_info)
+ async def _process_service_update(
+ self, zeroconf: HaZeroconf, service_type: str, name: str
+ ) -> None:
+ """Process a zeroconf update."""
+ async_service_info = AsyncServiceInfo(service_type, name)
+ await async_service_info.async_request(zeroconf, 3000)
+
+ info = info_from_service(async_service_info)
if not info:
# Prevent the browser thread from collapsing
_LOGGER.debug("Failed to get addresses for device %s", name)
@@ -353,7 +370,7 @@ class ZeroconfDiscovery:
# If we can handle it as a HomeKit discovery, we do that here.
if service_type in HOMEKIT_TYPES:
if pending_flow := handle_homekit(self.hass, self.homekit_models, info):
- self.flow_dispatcher.create(pending_flow)
+ self.flow_dispatcher.async_create(pending_flow)
# Continue on here as homekit_controller
# still needs to get updates on devices
# so it can see when the 'c#' field is updated.
@@ -415,7 +432,7 @@ class ZeroconfDiscovery:
"context": {"source": config_entries.SOURCE_ZEROCONF},
"data": info,
}
- self.flow_dispatcher.create(flow)
+ self.flow_dispatcher.async_create(flow)
def handle_homekit(
@@ -453,7 +470,7 @@ def handle_homekit(
return None
-def info_from_service(service: ServiceInfo) -> HaServiceInfo | None:
+def info_from_service(service: AsyncServiceInfo) -> HaServiceInfo | None:
"""Return prepared info from mDNS entries."""
properties: dict[str, Any] = {"_raw": {}}
diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json
index 030a970d77d..199275623dc 100644
--- a/homeassistant/components/zeroconf/manifest.json
+++ b/homeassistant/components/zeroconf/manifest.json
@@ -2,7 +2,7 @@
"domain": "zeroconf",
"name": "Zero-configuration networking (zeroconf)",
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
- "requirements": ["zeroconf==0.31.0"],
+ "requirements": ["zeroconf==0.32.1"],
"dependencies": ["network", "api"],
"codeowners": ["@bdraco"],
"quality_scale": "internal",
diff --git a/homeassistant/components/zeroconf/models.py b/homeassistant/components/zeroconf/models.py
index c09e6428f2a..ffa5e1a2ecf 100644
--- a/homeassistant/components/zeroconf/models.py
+++ b/homeassistant/components/zeroconf/models.py
@@ -1,10 +1,11 @@
"""Models for Zeroconf."""
-import asyncio
from typing import Any
-from zeroconf import DNSPointer, DNSRecord, ServiceBrowser, Zeroconf
-from zeroconf.asyncio import AsyncZeroconf
+from zeroconf import DNSAddress, DNSRecord, Zeroconf
+from zeroconf.asyncio import AsyncServiceBrowser, AsyncZeroconf
+
+TYPE_AAAA = 28
class HaZeroconf(Zeroconf):
@@ -19,33 +20,26 @@ class HaZeroconf(Zeroconf):
class HaAsyncZeroconf(AsyncZeroconf):
"""Home Assistant version of AsyncZeroconf."""
- def __init__( # pylint: disable=super-init-not-called
- self, *args: Any, **kwargs: Any
- ) -> None:
- """Wrap AsyncZeroconf."""
- self.zeroconf = HaZeroconf(*args, **kwargs)
- self.loop = asyncio.get_running_loop()
-
async def async_close(self) -> None:
"""Fake method to avoid integrations closing it."""
+ ha_async_close = AsyncZeroconf.async_close
-class HaServiceBrowser(ServiceBrowser):
+
+class HaAsyncServiceBrowser(AsyncServiceBrowser):
"""ServiceBrowser that only consumes DNSPointer records."""
- def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
- """Pre-Filter update_record to DNSPointers for the configured type."""
+ def __init__(self, ipv6: bool, *args: Any, **kwargs: Any) -> None:
+ """Create service browser that filters ipv6 if it is disabled."""
+ self.ipv6 = ipv6
+ super().__init__(*args, **kwargs)
- #
- # Each ServerBrowser currently runs in its own thread which
- # processes every A or AAAA record update per instance.
- #
- # As the list of zeroconf names we watch for grows, each additional
- # ServiceBrowser would process all the A and AAAA updates on the network.
- #
- # To avoid overwhemling the system we pre-filter here and only process
- # DNSPointers for the configured record name (type)
- #
- if record.name not in self.types or not isinstance(record, DNSPointer):
+ def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
+ """Pre-Filter AAAA records if IPv6 is not enabled."""
+ if (
+ not self.ipv6
+ and isinstance(record, DNSAddress)
+ and record.type == TYPE_AAAA
+ ):
return
super().update_record(zc, now, record)
diff --git a/homeassistant/components/zerproc/__init__.py b/homeassistant/components/zerproc/__init__.py
index 8d42c81162f..8643066f59c 100644
--- a/homeassistant/components/zerproc/__init__.py
+++ b/homeassistant/components/zerproc/__init__.py
@@ -17,7 +17,7 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Zerproc from a config entry."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py
index 053162010e8..403af7c6612 100644
--- a/homeassistant/components/zha/api.py
+++ b/homeassistant/components/zha/api.py
@@ -363,6 +363,7 @@ def cv_group_member(value: Any) -> GroupMember:
{
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]),
}
)
@@ -371,7 +372,8 @@ async def websocket_add_group(hass, connection, msg):
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
group_name = msg[GROUP_NAME]
members = msg.get(ATTR_MEMBERS)
- group = await zha_gateway.async_create_zigpy_group(group_name, members)
+ group_id = msg.get(GROUP_ID)
+ group = await zha_gateway.async_create_zigpy_group(group_name, members, group_id)
connection.send_result(msg[ID], group.group_info)
diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py
index 475b0c5d0b8..3aa11d85516 100644
--- a/homeassistant/components/zha/climate.py
+++ b/homeassistant/components/zha/climate.py
@@ -614,8 +614,12 @@ class CentralitePearl(ZenWithinThermostat):
manufacturers={
"_TZE200_ckud7u2l",
"_TZE200_ywdxldoj",
+ "_TZE200_cwnjrr72",
+ "_TZE200_b6wax7g0",
"_TYST11_ckud7u2l",
"_TYST11_ywdxldoj",
+ "_TYST11_cwnjrr72",
+ "_TYST11_b6wax7g0",
},
)
class MoesThermostat(Thermostat):
diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py
index dcc2a080a76..ecb65981637 100644
--- a/homeassistant/components/zha/core/const.py
+++ b/homeassistant/components/zha/core/const.py
@@ -137,10 +137,23 @@ CONF_RADIO_TYPE = "radio_type"
CONF_USB_PATH = "usb_path"
CONF_ZIGPY = "zigpy_config"
+CONF_CONSIDER_UNAVAILABLE_MAINS = "consider_unavailable_mains"
+CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS = 60 * 60 * 2 # 2 hours
+CONF_CONSIDER_UNAVAILABLE_BATTERY = "consider_unavailable_battery"
+CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours
+
CONF_ZHA_OPTIONS_SCHEMA = vol.Schema(
{
vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int,
vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean,
+ vol.Optional(
+ CONF_CONSIDER_UNAVAILABLE_MAINS,
+ default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS,
+ ): cv.positive_int,
+ vol.Optional(
+ CONF_CONSIDER_UNAVAILABLE_BATTERY,
+ default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY,
+ ): cv.positive_int,
}
)
diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py
index 37608287609..0c572bfba8a 100644
--- a/homeassistant/components/zha/core/device.py
+++ b/homeassistant/components/zha/core/device.py
@@ -55,6 +55,10 @@ from .const import (
CLUSTER_COMMANDS_SERVER,
CLUSTER_TYPE_IN,
CLUSTER_TYPE_OUT,
+ CONF_CONSIDER_UNAVAILABLE_BATTERY,
+ CONF_CONSIDER_UNAVAILABLE_MAINS,
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY,
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS,
CONF_ENABLE_IDENTIFY_ON_JOIN,
EFFECT_DEFAULT_VARIANT,
EFFECT_OKAY,
@@ -70,8 +74,6 @@ from .const import (
from .helpers import LogMixin, async_get_zha_config_value
_LOGGER = logging.getLogger(__name__)
-CONSIDER_UNAVAILABLE_MAINS = 60 * 60 * 2 # 2 hours
-CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours
_UPDATE_ALIVE_INTERVAL = (60, 90)
_CHECKIN_GRACE_PERIODS = 2
@@ -107,9 +109,20 @@ class ZHADevice(LogMixin):
)
if self.is_mains_powered:
- self._consider_unavailable_time = CONSIDER_UNAVAILABLE_MAINS
+ self.consider_unavailable_time = async_get_zha_config_value(
+ self._zha_gateway.config_entry,
+ ZHA_OPTIONS,
+ CONF_CONSIDER_UNAVAILABLE_MAINS,
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS,
+ )
else:
- self._consider_unavailable_time = CONSIDER_UNAVAILABLE_BATTERY
+ self.consider_unavailable_time = async_get_zha_config_value(
+ self._zha_gateway.config_entry,
+ ZHA_OPTIONS,
+ CONF_CONSIDER_UNAVAILABLE_BATTERY,
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY,
+ )
+
keep_alive_interval = random.randint(*_UPDATE_ALIVE_INTERVAL)
self.unsubs.append(
async_track_time_interval(
@@ -320,7 +333,7 @@ class ZHADevice(LogMixin):
return
difference = time.time() - self.last_seen
- if difference < self._consider_unavailable_time:
+ if difference < self.consider_unavailable_time:
self.update_available(True)
self._checkins_missed_count = 0
return
diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py
index 4a9e6c28203..491f1a29774 100644
--- a/homeassistant/components/zha/core/gateway.py
+++ b/homeassistant/components/zha/core/gateway.py
@@ -77,12 +77,7 @@ from .const import (
ZHA_GW_MSG_RAW_INIT,
RadioType,
)
-from .device import (
- CONSIDER_UNAVAILABLE_BATTERY,
- CONSIDER_UNAVAILABLE_MAINS,
- DeviceStatus,
- ZHADevice,
-)
+from .device import DeviceStatus, ZHADevice
from .group import GroupMember, ZHAGroup
from .registries import GROUP_ENTITY_DOMAINS
from .store import async_get_registry
@@ -185,17 +180,15 @@ class ZHAGateway:
delta_msg = "not known"
if zha_dev_entry and zha_dev_entry.last_seen is not None:
delta = round(time.time() - zha_dev_entry.last_seen)
- if zha_device.is_mains_powered:
- zha_device.available = delta < CONSIDER_UNAVAILABLE_MAINS
- else:
- zha_device.available = delta < CONSIDER_UNAVAILABLE_BATTERY
+ zha_device.available = delta < zha_device.consider_unavailable_time
delta_msg = f"{str(timedelta(seconds=delta))} ago"
_LOGGER.debug(
- "[%s](%s) restored as '%s', last seen: %s",
+ "[%s](%s) restored as '%s', last seen: %s, consider_unavailable_time: %s seconds",
zha_device.nwk,
zha_device.name,
"available" if zha_device.available else "unavailable",
delta_msg,
+ zha_device.consider_unavailable_time,
)
# update the last seen time for devices every 10 minutes to avoid thrashing
# writes and shutdown issues where storage isn't updated
@@ -623,13 +616,15 @@ class ZHAGateway:
zha_device.update_available(True)
async def async_create_zigpy_group(
- self, name: str, members: list[GroupMember]
+ self, name: str, members: list[GroupMember], group_id: int = None
) -> ZhaGroupType:
"""Create a new Zigpy Zigbee group."""
# we start with two to fill any gaps from a user removing existing groups
- group_id = 2
- while group_id in self.groups:
- group_id += 1
+
+ if group_id is None:
+ group_id = 2
+ while group_id in self.groups:
+ group_id += 1
# guard against group already existing
if self.async_get_group_by_name(name) is None:
diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py
index 35080c56921..71c5dcca908 100644
--- a/homeassistant/components/zha/cover.py
+++ b/homeassistant/components/zha/cover.py
@@ -177,6 +177,8 @@ class ZhaCover(ZhaEntity, CoverEntity):
class Shade(ZhaEntity, CoverEntity):
"""ZHA Shade."""
+ _attr_device_class = DEVICE_CLASS_SHADE
+
def __init__(
self,
unique_id: str,
@@ -199,11 +201,6 @@ class Shade(ZhaEntity, CoverEntity):
"""
return self._position
- @property
- def device_class(self) -> str | None:
- """Return the class of this device, from component DEVICE_CLASSES."""
- return DEVICE_CLASS_SHADE
-
@property
def is_closed(self) -> bool | None:
"""Return True if shade is closed."""
@@ -289,10 +286,7 @@ class Shade(ZhaEntity, CoverEntity):
class KeenVent(Shade):
"""Keen vent cover."""
- @property
- def device_class(self) -> str | None:
- """Return the class of this device, from component DEVICE_CLASSES."""
- return DEVICE_CLASS_DAMPER
+ _attr_device_class = DEVICE_CLASS_DAMPER
async def async_open_cover(self, **kwargs):
"""Open the cover."""
diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py
index 9d04d36f748..03bdc32e6a6 100644
--- a/homeassistant/components/zha/device_trigger.py
+++ b/homeassistant/components/zha/device_trigger.py
@@ -1,7 +1,7 @@
"""Provides device automations for ZHA devices that emit events."""
import voluptuous as vol
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.device_automation.exceptions import (
InvalidDeviceAutomationConfig,
)
@@ -16,7 +16,7 @@ DEVICE = "device"
DEVICE_IEEE = "device_ieee"
ZHA_EVENT = "zha_event"
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str}
)
diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json
index ec231ccc0e4..d37abea2310 100644
--- a/homeassistant/components/zha/manifest.json
+++ b/homeassistant/components/zha/manifest.json
@@ -4,13 +4,13 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [
- "bellows==0.24.0",
+ "bellows==0.25.0",
"pyserial==3.5",
"pyserial-asyncio==0.5",
- "zha-quirks==0.0.57",
+ "zha-quirks==0.0.59",
"zigpy-cc==0.5.2",
"zigpy-deconz==0.12.0",
- "zigpy==0.33.0",
+ "zigpy==0.35.1",
"zigpy-xbee==0.13.0",
"zigpy-zigate==0.7.3",
"zigpy-znp==0.5.1"
diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json
index 9ca6a9821b3..9abff4e83e2 100644
--- a/homeassistant/components/zha/strings.json
+++ b/homeassistant/components/zha/strings.json
@@ -33,7 +33,9 @@
"zha_options": {
"title": "Global Options",
"enable_identify_on_join": "Enable identify effect when devices join the network",
- "default_light_transition": "Default light transition time (seconds)"
+ "default_light_transition": "Default light transition time (seconds)",
+ "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)",
+ "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)"
},
"zha_alarm_options": {
"title": "Alarm Control Panel Options",
diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json
index 0320ea34f2c..2467db76709 100644
--- a/homeassistant/components/zha/translations/ca.json
+++ b/homeassistant/components/zha/translations/ca.json
@@ -41,6 +41,8 @@
"title": "Opcions del panell de control d'alarma"
},
"zha_options": {
+ "consider_unavailable_battery": "Considera els dispositius amb bateria com a no disponibles al cap de (segons)",
+ "consider_unavailable_mains": "Considera els dispositius connectats a la xarxa el\u00e8ctrica com a no disponibles al cap de (segons)",
"default_light_transition": "Temps de transici\u00f3 predeterminat (segons)",
"enable_identify_on_join": "Activa l'efecte d'identificaci\u00f3 quan els dispositius s'uneixin a la xarxa",
"title": "Opcions globals"
diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json
index bea46792003..48a84e712f2 100644
--- a/homeassistant/components/zha/translations/de.json
+++ b/homeassistant/components/zha/translations/de.json
@@ -41,6 +41,8 @@
"title": "Optionen f\u00fcr die Alarmsteuerung"
},
"zha_options": {
+ "consider_unavailable_battery": "Batteriebetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)",
+ "consider_unavailable_mains": "Netzbetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)",
"default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)",
"enable_identify_on_join": "Aktivieren Sie den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten",
"title": "Globale Optionen"
diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json
index 83dc56c75fc..e13aca2cfb1 100644
--- a/homeassistant/components/zha/translations/en.json
+++ b/homeassistant/components/zha/translations/en.json
@@ -41,6 +41,8 @@
"title": "Alarm Control Panel Options"
},
"zha_options": {
+ "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)",
+ "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)",
"default_light_transition": "Default light transition time (seconds)",
"enable_identify_on_join": "Enable identify effect when devices join the network",
"title": "Global Options"
diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json
index 5655d67fd34..4753834a493 100644
--- a/homeassistant/components/zha/translations/es.json
+++ b/homeassistant/components/zha/translations/es.json
@@ -41,6 +41,8 @@
"title": "Opciones del panel de control de la alarma"
},
"zha_options": {
+ "consider_unavailable_battery": "Considere que los dispositivos alimentados por bater\u00eda no est\u00e1n disponibles despu\u00e9s de (segundos)",
+ "consider_unavailable_mains": "Considere que los dispositivos alimentados por la red el\u00e9ctrica no est\u00e1n disponibles despu\u00e9s de (segundos)",
"default_light_transition": "Tiempo de transici\u00f3n de la luz por defecto (segundos)",
"enable_identify_on_join": "Activar el efecto de identificaci\u00f3n cuando los dispositivos se unen a la red",
"title": "Opciones globales"
diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json
index afbf2180ef6..311a6378fcb 100644
--- a/homeassistant/components/zha/translations/et.json
+++ b/homeassistant/components/zha/translations/et.json
@@ -41,6 +41,8 @@
"title": "Valvekeskuse juhtpaneeli s\u00e4tted"
},
"zha_options": {
+ "consider_unavailable_battery": "Arvesta, et patareitoitega seadmed pole p\u00e4rast (sekundit) saadaval",
+ "consider_unavailable_mains": "Arvesta, et v\u00f5rgutoitega seadmed pole p\u00e4rast (sekundit) saadaval",
"default_light_transition": "Heleduse vaike\u00fclemineku aeg (sekundites)",
"enable_identify_on_join": "Luba tuvastamine kui seadmed liituvad v\u00f5rguga",
"title": "\u00dcldised valikud"
diff --git a/homeassistant/components/zha/translations/he.json b/homeassistant/components/zha/translations/he.json
index 2ede9ae4430..fa40de672e2 100644
--- a/homeassistant/components/zha/translations/he.json
+++ b/homeassistant/components/zha/translations/he.json
@@ -1,9 +1,50 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea."
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "flow_title": "{name}",
"step": {
"port_config": {
"title": "\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea"
+ },
+ "user": {
+ "title": "ZHA"
}
}
+ },
+ "config_panel": {
+ "zha_options": {
+ "consider_unavailable_battery": "\u05e9\u05e7\u05d5\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e2\u05dc\u05d9\u05dd \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d5\u05dc\u05dc\u05d4 \u05db\u05dc\u05d0 \u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05dc\u05d0\u05d7\u05e8 (\u05e9\u05e0\u05d9\u05d5\u05ea)",
+ "consider_unavailable_mains": "\u05e9\u05e7\u05d5\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e2\u05dc\u05d9\u05dd \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e8\u05e9\u05ea \u05d7\u05e9\u05de\u05dc \u05dc\u05d0 \u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05dc\u05d0\u05d7\u05e8 (\u05e9\u05e0\u05d9\u05d5\u05ea)",
+ "enable_identify_on_join": "\u05d0\u05e4\u05e9\u05e8 \u05d0\u05e4\u05e7\u05d8 \u05d6\u05d9\u05d4\u05d5\u05d9 \u05db\u05d0\u05e9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05de\u05e6\u05d8\u05e8\u05e4\u05d9\u05dd \u05dc\u05e8\u05e9\u05ea",
+ "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05db\u05dc\u05dc\u05d9\u05d5\u05ea"
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "\u05e9\u05e0\u05d9 \u05d4\u05db\u05e4\u05ea\u05d5\u05e8\u05d9\u05dd",
+ "button_1": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e8\u05d0\u05e9\u05d5\u05df",
+ "button_2": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05e0\u05d9",
+ "button_3": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05dc\u05d9\u05e9\u05d9",
+ "button_4": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e8\u05d1\u05d9\u05e2\u05d9",
+ "button_5": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05d7\u05de\u05d9\u05e9\u05d9",
+ "button_6": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05d9\u05e9\u05d9",
+ "close": "\u05e1\u05d2\u05d5\u05e8",
+ "dim_down": "\u05e2\u05de\u05e2\u05d5\u05dd \u05dc\u05de\u05d8\u05d4",
+ "dim_up": "\u05e2\u05de\u05e2\u05d5\u05dd \u05dc\u05de\u05e2\u05dc\u05d4",
+ "left": "\u05e9\u05de\u05d0\u05dc",
+ "open": "\u05e4\u05ea\u05d5\u05d7",
+ "right": "\u05d9\u05de\u05d9\u05df",
+ "turn_off": "\u05db\u05d1\u05d4",
+ "turn_on": "\u05d4\u05e4\u05e2\u05dc"
+ },
+ "trigger_type": {
+ "device_dropped": "\u05d4\u05d4\u05ea\u05e7\u05df \u05d4\u05d5\u05e9\u05de\u05d8",
+ "device_offline": "\u05d4\u05ea\u05e7\u05df \u05dc\u05d0 \u05de\u05e7\u05d5\u05d5\u05df"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json
index aaa41429fde..896d4fbad30 100644
--- a/homeassistant/components/zha/translations/hu.json
+++ b/homeassistant/components/zha/translations/hu.json
@@ -6,7 +6,7 @@
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
- "flow_title": "ZHA: {n\u00e9v}",
+ "flow_title": "{name}",
"step": {
"port_config": {
"data": {
diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json
index 247f6b4027e..4cdcdd654cc 100644
--- a/homeassistant/components/zha/translations/it.json
+++ b/homeassistant/components/zha/translations/it.json
@@ -41,6 +41,8 @@
"title": "Opzioni del pannello di controllo degli allarmi"
},
"zha_options": {
+ "consider_unavailable_battery": "Considera i dispositivi alimentati a batteria non disponibili dopo (secondi)",
+ "consider_unavailable_mains": "Considera i dispositivi alimentati dalla rete non disponibili dopo (secondi)",
"default_light_transition": "Tempo di transizione della luce predefinito (secondi)",
"enable_identify_on_join": "Abilita l'effetto di identificazione quando i dispositivi si uniscono alla rete",
"title": "Opzioni globali"
@@ -82,7 +84,7 @@
"device_offline": "Dispositivo offline",
"device_rotated": "Dispositivo ruotato \" {subtype} \"",
"device_shaken": "Dispositivo in vibrazione",
- "device_slid": "Dispositivo scivolato \"{sottotipo}\"",
+ "device_slid": "Dispositivo scivolato \"{subtype}\"",
"device_tilted": "Dispositivo inclinato",
"remote_button_alt_double_press": "Pulsante \"{subtype}\" cliccato due volte (modalit\u00e0 Alternata)",
"remote_button_alt_long_press": "Pulsante \"{subtype}\" premuto continuamente (modalit\u00e0 Alternata)",
diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json
index 86a3f7a7f69..f4cdb8f642b 100644
--- a/homeassistant/components/zha/translations/nl.json
+++ b/homeassistant/components/zha/translations/nl.json
@@ -41,6 +41,8 @@
"title": "Alarm bedieningspaneel Opties"
},
"zha_options": {
+ "consider_unavailable_battery": "Overweeg apparaten met batterijvoeding als onbeschikbaar na (seconden)",
+ "consider_unavailable_mains": "Beschouw apparaten op netvoeding als onbeschikbaar na (seconden)",
"default_light_transition": "Standaard licht transitietijd (seconden)",
"enable_identify_on_join": "Schakel het identificatie-effect in wanneer apparaten in het netwerk komen",
"title": "Globale opties"
diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json
index 087b4adb2c3..efa6c06a067 100644
--- a/homeassistant/components/zha/translations/no.json
+++ b/homeassistant/components/zha/translations/no.json
@@ -41,6 +41,8 @@
"title": "Alternativer for alarmkontrollpanel"
},
"zha_options": {
+ "consider_unavailable_battery": "Vurder batteridrevne enheter som utilgjengelige etter (sekunder)",
+ "consider_unavailable_mains": "Tenk p\u00e5 str\u00f8mnettet som ikke er tilgjengelig etter (sekunder)",
"default_light_transition": "Standard lysovergangstid (sekunder)",
"enable_identify_on_join": "Aktiver identifiseringseffekt n\u00e5r enheter blir med i nettverket",
"title": "Globale alternativer"
diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json
index dce671771f3..8c726fc349f 100644
--- a/homeassistant/components/zha/translations/pl.json
+++ b/homeassistant/components/zha/translations/pl.json
@@ -41,6 +41,8 @@
"title": "Opcje panelu alarmowego"
},
"zha_options": {
+ "consider_unavailable_battery": "Uznaj urz\u0105dzenia zasilane bateryjnie za niedost\u0119pne po (sekundach)",
+ "consider_unavailable_mains": "Uznaj urz\u0105dzenia zasilane z gniazdka za niedost\u0119pne po (sekundach)",
"default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)",
"enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci",
"title": "Opcje og\u00f3lne"
diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json
index 291f5c0eea1..6f88ca16ae9 100644
--- a/homeassistant/components/zha/translations/ru.json
+++ b/homeassistant/components/zha/translations/ru.json
@@ -41,6 +41,8 @@
"title": "\u041e\u043f\u0446\u0438\u0438 \u043f\u0430\u043d\u0435\u043b\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439"
},
"zha_options": {
+ "consider_unavailable_battery": "\u0412\u0440\u0435\u043c\u044f, \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438 \u0431\u0443\u0434\u0435\u0442 \u0441\u0447\u0438\u0442\u0430\u0442\u044c\u0441\u044f \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)",
+ "consider_unavailable_mains": "\u0412\u0440\u0435\u043c\u044f, \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u0431\u0443\u0434\u0435\u0442 \u0441\u0447\u0438\u0442\u0430\u0442\u044c\u0441\u044f \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)",
"default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)",
"enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438",
"title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438"
diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json
index 43cc366ca35..d219e311791 100644
--- a/homeassistant/components/zha/translations/zh-Hant.json
+++ b/homeassistant/components/zha/translations/zh-Hant.json
@@ -41,6 +41,8 @@
"title": "\u8b66\u6212\u63a7\u5236\u9762\u677f\u9078\u9805"
},
"zha_options": {
+ "consider_unavailable_battery": "\u5c07\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09",
+ "consider_unavailable_mains": "\u5c07\u4e3b\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09",
"default_light_transition": "\u9810\u8a2d\u71c8\u5149\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09",
"enable_identify_on_join": "\u7576\u88dd\u7f6e\u52a0\u5165\u7db2\u8def\u6642\u3001\u958b\u555f\u8b58\u5225\u6548\u679c",
"title": "Global \u9078\u9805"
diff --git a/homeassistant/components/zodiac/__init__.py b/homeassistant/components/zodiac/__init__.py
index d00cc560f22..c19b7a45ac2 100644
--- a/homeassistant/components/zodiac/__init__.py
+++ b/homeassistant/components/zodiac/__init__.py
@@ -3,6 +3,7 @@ import voluptuous as vol
from homeassistant.core import HomeAssistant
from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
@@ -12,7 +13,7 @@ CONFIG_SCHEMA = vol.Schema(
)
-async def async_setup(hass: HomeAssistant, config: dict):
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the zodiac component."""
hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
diff --git a/homeassistant/components/zodiac/sensor.py b/homeassistant/components/zodiac/sensor.py
index 4c037a7aa02..80a4f782915 100644
--- a/homeassistant/components/zodiac/sensor.py
+++ b/homeassistant/components/zodiac/sensor.py
@@ -1,5 +1,10 @@
"""Support for tracking the zodiac sign."""
+from __future__ import annotations
+
from homeassistant.components.sensor import SensorEntity
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.dt import as_local, utcnow
from .const import (
@@ -154,7 +159,12 @@ ZODIAC_ICONS = {
}
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
"""Set up the Zodiac sensor platform."""
if discovery_info is None:
return
@@ -165,42 +175,42 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class ZodiacSensor(SensorEntity):
"""Representation of a Zodiac sensor."""
- def __init__(self):
+ def __init__(self) -> None:
"""Initialize the zodiac sensor."""
- self._attrs = None
- self._state = None
+ self._attrs: dict[str, str] = {}
+ self._state: str = ""
@property
- def unique_id(self):
+ def unique_id(self) -> str:
"""Return a unique ID."""
return DOMAIN
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the entity."""
return "Zodiac"
@property
- def device_class(self):
+ def device_class(self) -> str:
"""Return the device class of the entity."""
return "zodiac__sign"
@property
- def state(self):
+ def state(self) -> str:
"""Return the state of the device."""
return self._state
@property
- def icon(self):
- """Icon to use in the frontend, if any."""
+ def icon(self) -> str | None:
+ """Icon to use in the frontend."""
return ZODIAC_ICONS.get(self._state)
@property
- def extra_state_attributes(self):
+ def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes."""
return self._attrs
- async def async_update(self):
+ async def async_update(self) -> None:
"""Get the time and updates the state."""
today = as_local(utcnow()).date()
diff --git a/homeassistant/components/zodiac/translations/sensor.he.json b/homeassistant/components/zodiac/translations/sensor.he.json
new file mode 100644
index 00000000000..8ec13a3fcf0
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.he.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "\u05d3\u05dc\u05d9",
+ "aries": "\u05d8\u05dc\u05d4",
+ "cancer": "\u05e1\u05e8\u05d8\u05df",
+ "capricorn": "\u05d2\u05d3\u05d9",
+ "gemini": "\u05ea\u05d0\u05d5\u05de\u05d9\u05dd",
+ "leo": "\u05d0\u05e8\u05d9\u05d4",
+ "libra": "\u05de\u05d0\u05d6\u05e0\u05d9\u05d9\u05dd",
+ "pisces": "\u05d3\u05d2\u05d9\u05dd",
+ "sagittarius": "\u05e7\u05e9\u05ea",
+ "scorpio": "\u05e2\u05e7\u05e8\u05d1",
+ "taurus": "\u05e9\u05d5\u05e8",
+ "virgo": "\u05d1\u05ea\u05d5\u05dc\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/translations/he.json b/homeassistant/components/zone/translations/he.json
index b6a2a30b625..99bbed91c0f 100644
--- a/homeassistant/components/zone/translations/he.json
+++ b/homeassistant/components/zone/translations/he.json
@@ -6,7 +6,7 @@
"step": {
"init": {
"data": {
- "icon": "\u05e1\u05de\u05dc",
+ "icon": "\u05e1\u05de\u05dc\u05d9\u05dc",
"latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
"longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
"name": "\u05e9\u05dd",
diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py
index db5ca2cf01b..eb084fe1874 100644
--- a/homeassistant/components/zone/trigger.py
+++ b/homeassistant/components/zone/trigger.py
@@ -21,7 +21,7 @@ DEFAULT_EVENT = EVENT_ENTER
_EVENT_DESCRIPTION = {EVENT_ENTER: "entering", EVENT_LEAVE: "leaving"}
-TRIGGER_SCHEMA = vol.Schema(
+TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "zone",
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
@@ -37,7 +37,7 @@ async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type: str = "zone"
) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration."""
- trigger_id = automation_info.get("trigger_id") if automation_info else None
+ trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
entity_id = config.get(CONF_ENTITY_ID)
zone_entity_id = config.get(CONF_ZONE)
event = config.get(CONF_EVENT)
@@ -74,6 +74,7 @@ async def async_attach_trigger(
job,
{
"trigger": {
+ **trigger_data,
"platform": platform_type,
"entity_id": entity,
"from_state": from_s,
@@ -81,7 +82,6 @@ async def async_attach_trigger(
"zone": zone_state,
"event": event,
"description": description,
- "id": trigger_id,
}
},
to_s.context,
diff --git a/homeassistant/components/zoneminder/translations/de.json b/homeassistant/components/zoneminder/translations/de.json
index 5fa5d0a5234..af053e59ec3 100644
--- a/homeassistant/components/zoneminder/translations/de.json
+++ b/homeassistant/components/zoneminder/translations/de.json
@@ -1,22 +1,33 @@
{
"config": {
"abort": {
+ "auth_fail": "Benutzername oder Passwort sind falsch.",
"cannot_connect": "Verbindung fehlgeschlagen",
+ "connection_error": "Es konnte keine Verbindung zu einem ZoneMinder-Server hergestellt werden.",
"invalid_auth": "Ung\u00fcltige Authentifizierung"
},
+ "create_entry": {
+ "default": "ZoneMinder-Server hinzugef\u00fcgt."
+ },
"error": {
+ "auth_fail": "Benutzername oder Passwort sind falsch.",
"cannot_connect": "Verbindung fehlgeschlagen",
+ "connection_error": "Es konnte keine Verbindung zu einem ZoneMinder-Server hergestellt werden.",
"invalid_auth": "Ung\u00fcltige Authentifizierung"
},
"flow_title": "ZoneMinder",
"step": {
"user": {
"data": {
+ "host": "Host und Port (z. B. 10.10.0.4:8010)",
"password": "Passwort",
+ "path": "ZM-Pfad",
+ "path_zms": "ZMS-Pfad",
"ssl": "Nutzt ein SSL-Zertifikat",
"username": "Benutzername",
"verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
- }
+ },
+ "title": "ZoneMinder Server hinzuf\u00fcgen."
}
}
}
diff --git a/homeassistant/components/zoneminder/translations/he.json b/homeassistant/components/zoneminder/translations/he.json
new file mode 100644
index 00000000000..208792441f7
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/he.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "\u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05d0\u05d5 \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d9\u05dd.",
+ "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"
+ },
+ "error": {
+ "auth_fail": "\u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05d0\u05d5 \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d9\u05dd.",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7 \u05d5\u05d9\u05e6\u05d9\u05d0\u05d4 (\u05dc\u05d3\u05d5\u05d2' 10.10.0.4:8010)",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9",
+ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave/translations/de.json b/homeassistant/components/zwave/translations/de.json
index 488580cb18a..0a82d5b0bc7 100644
--- a/homeassistant/components/zwave/translations/de.json
+++ b/homeassistant/components/zwave/translations/de.json
@@ -13,7 +13,7 @@
"network_key": "Netzwerkschl\u00fcssel (leer lassen, um automatisch zu generieren)",
"usb_path": "USB-Ger\u00e4t Pfad"
},
- "description": "Informationen zu den Konfigurationsvariablen findest du unter https://www.home-assistant.io/docs/z-wave/installation/"
+ "description": "Diese Integration wird nicht mehr gepflegt. Verwenden Sie bei Neuinstallationen stattdessen Z-Wave JS.\n\nSiehe https://www.home-assistant.io/docs/z-wave/installation/ f\u00fcr Informationen zu den Konfigurationsvariablen"
}
}
},
diff --git a/homeassistant/components/zwave/translations/he.json b/homeassistant/components/zwave/translations/he.json
index 4ed45b0711f..585b696b496 100644
--- a/homeassistant/components/zwave/translations/he.json
+++ b/homeassistant/components/zwave/translations/he.json
@@ -1,4 +1,17 @@
{
+ "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",
diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py
index 520495d5071..fee4da743c8 100644
--- a/homeassistant/components/zwave_js/__init__.py
+++ b/homeassistant/components/zwave_js/__init__.py
@@ -15,6 +15,7 @@ from zwave_js_server.model.notification import (
)
from zwave_js_server.model.value import Value, ValueNotification
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_DEVICE_ID,
@@ -206,6 +207,25 @@ async def async_setup_entry( # noqa: C901
async def async_on_node_added(node: ZwaveNode) -> None:
"""Handle node added event."""
+ platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP]
+
+ # We need to set up the sensor platform if it hasn't already been setup in
+ # order to create the node status sensor
+ if SENSOR_DOMAIN not in platform_setup_tasks:
+ platform_setup_tasks[SENSOR_DOMAIN] = hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, SENSOR_DOMAIN)
+ )
+
+ # This guard ensures that concurrent runs of this function all await the
+ # platform setup task
+ if not platform_setup_tasks[SENSOR_DOMAIN].done():
+ await platform_setup_tasks[SENSOR_DOMAIN]
+
+ # Create a node status sensor for each device
+ async_dispatcher_send(
+ hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node
+ )
+
# we only want to run discovery when the node has reached ready state,
# otherwise we'll have all kinds of missing info issues.
if node.ready:
@@ -362,7 +382,7 @@ async def async_setup_entry( # noqa: C901
entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = False
entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = False
- services = ZWaveServices(hass, ent_reg)
+ services = ZWaveServices(hass, ent_reg, dev_reg)
services.async_register()
# Set up websocket API
@@ -374,7 +394,7 @@ async def async_setup_entry( # noqa: C901
async def handle_ha_shutdown(event: Event) -> None:
"""Handle HA shutdown."""
- await disconnect_client(hass, entry, client, listen_task, platform_task)
+ await disconnect_client(hass, entry)
listen_task = asyncio.create_task(
client_listen(hass, entry, client, driver_ready)
@@ -470,14 +490,12 @@ async def client_listen(
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
-async def disconnect_client(
- hass: HomeAssistant,
- entry: ConfigEntry,
- client: ZwaveClient,
- listen_task: asyncio.Task,
- platform_task: asyncio.Task,
-) -> None:
+async def disconnect_client(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Disconnect client."""
+ data = hass.data[DOMAIN][entry.entry_id]
+ client: ZwaveClient = data[DATA_CLIENT]
+ listen_task: asyncio.Task = data[DATA_CLIENT_LISTEN_TASK]
+ platform_task: asyncio.Task = data[DATA_START_PLATFORM_TASK]
listen_task.cancel()
platform_task.cancel()
platform_setup_tasks = (
@@ -495,7 +513,7 @@ async def disconnect_client(
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- info = hass.data[DOMAIN].pop(entry.entry_id)
+ info = hass.data[DOMAIN][entry.entry_id]
for unsub in info[DATA_UNSUBSCRIBE]:
unsub()
@@ -513,13 +531,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = all(await asyncio.gather(*tasks))
if DATA_CLIENT_LISTEN_TASK in info:
- await disconnect_client(
- hass,
- entry,
- info[DATA_CLIENT],
- info[DATA_CLIENT_LISTEN_TASK],
- platform_task=info[DATA_START_PLATFORM_TASK],
- )
+ await disconnect_client(hass, entry)
+
+ hass.data[DOMAIN].pop(entry.entry_id)
if entry.data.get(CONF_USE_ADDON) and entry.disabled_by:
addon_manager: AddonManager = get_addon_manager(hass)
diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py
index ff74b5d5a44..a0caaa15488 100644
--- a/homeassistant/components/zwave_js/addon.py
+++ b/homeassistant/components/zwave_js/addon.py
@@ -12,6 +12,7 @@ from homeassistant.components.hassio import (
async_get_addon_discovery_info,
async_get_addon_info,
async_install_addon,
+ async_restart_addon,
async_set_addon_options,
async_start_addon,
async_stop_addon,
@@ -89,6 +90,7 @@ class AddonManager:
"""Set up the add-on manager."""
self._hass = hass
self._install_task: asyncio.Task | None = None
+ self._restart_task: asyncio.Task | None = None
self._start_task: asyncio.Task | None = None
self._update_task: asyncio.Task | None = None
@@ -222,6 +224,11 @@ class AddonManager:
"""Start the Z-Wave JS add-on."""
await async_start_addon(self._hass, ADDON_SLUG)
+ @api_error("Failed to restart the Z-Wave JS add-on")
+ async def async_restart_addon(self) -> None:
+ """Restart the Z-Wave JS add-on."""
+ await async_restart_addon(self._hass, ADDON_SLUG)
+
@callback
def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task:
"""Schedule a task that starts the Z-Wave JS add-on.
@@ -235,6 +242,19 @@ class AddonManager:
)
return self._start_task
+ @callback
+ def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task:
+ """Schedule a task that restarts the Z-Wave JS add-on.
+
+ Only schedule a new restart task if the there's no running task.
+ """
+ if not self._restart_task or self._restart_task.done():
+ LOGGER.info("Restarting Z-Wave JS add-on")
+ self._restart_task = self._async_schedule_addon_operation(
+ self.async_restart_addon, catch_error=catch_error
+ )
+ return self._restart_task
+
@api_error("Failed to stop the Z-Wave JS add-on")
async def async_stop_addon(self) -> None:
"""Stop the Z-Wave JS add-on."""
diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py
index ffd00919941..5cf98d44802 100644
--- a/homeassistant/components/zwave_js/api.py
+++ b/homeassistant/components/zwave_js/api.py
@@ -51,6 +51,7 @@ from .const import (
EVENT_DEVICE_ADDED_TO_REGISTRY,
)
from .helpers import async_enable_statistics, update_data_collection_preference
+from .services import BITMASK_SCHEMA
# general API constants
ID = "id"
@@ -138,6 +139,7 @@ def async_register_api(hass: HomeAssistant) -> None:
"""Register all of our api endpoints."""
websocket_api.async_register_command(hass, websocket_network_status)
websocket_api.async_register_command(hass, websocket_node_status)
+ websocket_api.async_register_command(hass, websocket_node_state)
websocket_api.async_register_command(hass, websocket_node_metadata)
websocket_api.async_register_command(hass, websocket_ping_node)
websocket_api.async_register_command(hass, websocket_add_node)
@@ -157,13 +159,14 @@ def async_register_api(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, websocket_heal_node)
websocket_api.async_register_command(hass, websocket_set_config_parameter)
websocket_api.async_register_command(hass, websocket_get_config_parameters)
- websocket_api.async_register_command(hass, websocket_subscribe_logs)
+ websocket_api.async_register_command(hass, websocket_subscribe_log_updates)
websocket_api.async_register_command(hass, websocket_update_log_config)
websocket_api.async_register_command(hass, websocket_get_log_config)
websocket_api.async_register_command(
hass, websocket_update_data_collection_preference
)
websocket_api.async_register_command(hass, websocket_data_collection_status)
+ websocket_api.async_register_command(hass, websocket_version_info)
websocket_api.async_register_command(hass, websocket_abort_firmware_update)
websocket_api.async_register_command(
hass, websocket_subscribe_firmware_update_status
@@ -253,6 +256,29 @@ async def websocket_node_status(
)
+@websocket_api.require_admin
+@websocket_api.websocket_command(
+ {
+ vol.Required(TYPE): "zwave_js/node_state",
+ vol.Required(ENTRY_ID): str,
+ vol.Required(NODE_ID): int,
+ }
+)
+@websocket_api.async_response
+@async_get_node
+async def websocket_node_state(
+ hass: HomeAssistant,
+ connection: ActiveConnection,
+ msg: dict,
+ node: Node,
+) -> None:
+ """Get the state data of a Z-Wave JS node."""
+ connection.send_result(
+ msg[ID],
+ node.data,
+ )
+
+
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/node_metadata",
@@ -900,7 +926,7 @@ async def websocket_refresh_node_cc_values(
vol.Required(NODE_ID): int,
vol.Required(PROPERTY): int,
vol.Optional(PROPERTY_KEY): int,
- vol.Required(VALUE): int,
+ vol.Required(VALUE): vol.Any(int, BITMASK_SCHEMA),
}
)
@websocket_api.async_response
@@ -996,13 +1022,13 @@ def filename_is_present_if_logging_to_file(obj: dict) -> dict:
@websocket_api.require_admin
@websocket_api.websocket_command(
{
- vol.Required(TYPE): "zwave_js/subscribe_logs",
+ vol.Required(TYPE): "zwave_js/subscribe_log_updates",
vol.Required(ENTRY_ID): str,
}
)
@websocket_api.async_response
@async_get_entry
-async def websocket_subscribe_logs(
+async def websocket_subscribe_log_updates(
hass: HomeAssistant,
connection: ActiveConnection,
msg: dict,
@@ -1016,24 +1042,44 @@ async def websocket_subscribe_logs(
def async_cleanup() -> None:
"""Remove signal listeners."""
hass.async_create_task(driver.async_stop_listening_logs())
- unsub()
+ for unsub in unsubs:
+ unsub()
@callback
- def forward_event(event: dict) -> None:
+ def log_messages(event: dict) -> None:
log_msg: LogMessage = event["log_message"]
connection.send_message(
websocket_api.event_message(
msg[ID],
{
- "timestamp": log_msg.timestamp,
- "level": log_msg.level,
- "primary_tags": log_msg.primary_tags,
- "message": log_msg.formatted_message,
+ "type": "log_message",
+ "log_message": {
+ "timestamp": log_msg.timestamp,
+ "level": log_msg.level,
+ "primary_tags": log_msg.primary_tags,
+ "message": log_msg.formatted_message,
+ },
},
)
)
- unsub = driver.on("logging", forward_event)
+ @callback
+ def log_config_updates(event: dict) -> None:
+ log_config: LogConfig = event["log_config"]
+ connection.send_message(
+ websocket_api.event_message(
+ msg[ID],
+ {
+ "type": "log_config",
+ "log_config": dataclasses.asdict(log_config),
+ },
+ )
+ )
+
+ unsubs = [
+ driver.on("logging", log_messages),
+ driver.on("log config updated", log_config_updates),
+ ]
connection.subscriptions[msg["id"]] = async_cleanup
await driver.async_start_listening_logs()
@@ -1100,10 +1146,9 @@ async def websocket_get_log_config(
client: Client,
) -> None:
"""Get log configuration for the Z-Wave JS driver."""
- result = await client.driver.async_get_log_config()
connection.send_result(
msg[ID],
- dataclasses.asdict(result),
+ dataclasses.asdict(client.driver.log_config),
)
@@ -1170,6 +1215,8 @@ class DumpView(HomeAssistantView):
async def get(self, request: web.Request, config_entry_id: str) -> web.Response:
"""Dump the state of Z-Wave."""
+ if not request["hass_user"].is_admin:
+ raise Unauthorized()
hass = request.app["hass"]
if config_entry_id not in hass.data[DOMAIN]:
@@ -1188,6 +1235,35 @@ class DumpView(HomeAssistantView):
)
+@websocket_api.require_admin
+@websocket_api.websocket_command(
+ {
+ vol.Required(TYPE): "zwave_js/version_info",
+ vol.Required(ENTRY_ID): str,
+ },
+)
+@websocket_api.async_response
+@async_get_entry
+async def websocket_version_info(
+ hass: HomeAssistant,
+ connection: ActiveConnection,
+ msg: dict,
+ entry: ConfigEntry,
+ client: Client,
+) -> None:
+ """Get version info from the Z-Wave JS server."""
+ version_info = {
+ "driver_version": client.version.driver_version,
+ "server_version": client.version.server_version,
+ "min_schema_version": client.version.min_schema_version,
+ "max_schema_version": client.version.max_schema_version,
+ }
+ connection.send_result(
+ msg[ID],
+ version_info,
+ )
+
+
@websocket_api.require_admin
@websocket_api.websocket_command(
{
@@ -1287,7 +1363,7 @@ class FirmwareUploadView(HomeAssistantView):
raise web_exceptions.HTTPBadRequest
entry = hass.config_entries.async_get_entry(config_entry_id)
- client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT]
+ client: Client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT]
node = client.driver.controller.nodes.get(int(node_id))
if not node:
raise web_exceptions.HTTPNotFound
diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py
index ad186b69fe4..537f4f8e49e 100644
--- a/homeassistant/components/zwave_js/binary_sensor.py
+++ b/homeassistant/components/zwave_js/binary_sensor.py
@@ -269,7 +269,20 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
) -> None:
"""Initialize a ZWaveBooleanBinarySensor entity."""
super().__init__(config_entry, client, info)
- self._name = self.generate_name(include_value_name=True)
+
+ # Entity class attributes
+ self._attr_name = self.generate_name(include_value_name=True)
+ self._attr_device_class = (
+ DEVICE_CLASS_BATTERY
+ if self.info.primary_value.command_class == CommandClass.BATTERY
+ else None
+ )
+ # Legacy binary sensors are phased out (replaced by notification sensors)
+ # Disable by default to not confuse users
+ self._attr_entity_registry_enabled_default = bool(
+ self.info.primary_value.command_class != CommandClass.SENSOR_BINARY
+ or self.info.node.device_class.generic.key == 0x20
+ )
@property
def is_on(self) -> bool | None:
@@ -278,23 +291,6 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
return None
return bool(self.info.primary_value.value)
- @property
- def device_class(self) -> str | None:
- """Return device class."""
- if self.info.primary_value.command_class == CommandClass.BATTERY:
- return DEVICE_CLASS_BATTERY
- return None
-
- @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
- return bool(
- self.info.primary_value.command_class != CommandClass.SENSOR_BINARY
- or self.info.node.device_class.generic.key == 0x20
- )
-
class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
"""Representation of a Z-Wave binary_sensor from Notification CommandClass."""
@@ -309,13 +305,20 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
"""Initialize a ZWaveNotificationBinarySensor entity."""
super().__init__(config_entry, client, info)
self.state_key = state_key
- self._name = self.generate_name(
+ # check if we have a custom mapping for this value
+ self._mapping_info = self._get_sensor_mapping()
+
+ # Entity class attributes
+ self._attr_name = self.generate_name(
include_value_name=True,
alternate_value_name=self.info.primary_value.property_name,
additional_info=[self.info.primary_value.metadata.states[self.state_key]],
)
- # check if we have a custom mapping for this value
- self._mapping_info = self._get_sensor_mapping()
+ self._attr_device_class = self._mapping_info.get("device_class")
+ self._attr_unique_id = f"{self._attr_unique_id}.{self.state_key}"
+ self._attr_entity_registry_enabled_default = (
+ True if not self._mapping_info else self._mapping_info.get("enabled", True)
+ )
@property
def is_on(self) -> bool | None:
@@ -324,23 +327,6 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
return None
return int(self.info.primary_value.value) == int(self.state_key)
- @property
- def device_class(self) -> str | None:
- """Return device class."""
- return self._mapping_info.get("device_class")
-
- @property
- def unique_id(self) -> str:
- """Return unique id for this entity."""
- return f"{super().unique_id}.{self.state_key}"
-
- @property
- def entity_registry_enabled_default(self) -> bool:
- """Return if the entity should be enabled when first added to the entity registry."""
- if not self._mapping_info:
- return True
- return self._mapping_info.get("enabled", True)
-
@callback
def _get_sensor_mapping(self) -> NotificationSensorMapping:
"""Try to get a device specific mapping for this sensor."""
@@ -366,7 +352,15 @@ class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
super().__init__(config_entry, client, info)
# check if we have a custom mapping for this value
self._mapping_info = self._get_sensor_mapping()
- self._name = self.generate_name(include_value_name=True)
+
+ # Entity class attributes
+ self._attr_name = self.generate_name(include_value_name=True)
+ self._attr_device_class = self._mapping_info.get("device_class")
+ # We hide some more advanced sensors by default to not overwhelm users
+ # unless explicitly stated in a mapping, assume deisabled by default
+ self._attr_entity_registry_enabled_default = self._mapping_info.get(
+ "enabled", False
+ )
@property
def is_on(self) -> bool | None:
@@ -375,18 +369,6 @@ class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
return None
return self.info.primary_value.value in self._mapping_info["on_states"]
- @property
- def device_class(self) -> str | None:
- """Return device class."""
- return self._mapping_info.get("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 some more advanced sensors by default to not overwhelm users
- # unless explicitly stated in a mapping, assume deisabled by default
- return self._mapping_info.get("enabled", False)
-
@callback
def _get_sensor_mapping(self) -> PropertySensorMapping:
"""Try to get a device specific mapping for this sensor."""
diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py
index 4ef13276fbe..43363538500 100644
--- a/homeassistant/components/zwave_js/climate.py
+++ b/homeassistant/components/zwave_js/climate.py
@@ -469,15 +469,15 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target hvac mode."""
- if not self._current_mode:
- # Thermostat(valve) with no support for setting a mode
- raise ValueError(
- f"Thermostat {self.entity_id} does not support setting a mode"
- )
- hvac_mode_value = self._hvac_modes.get(hvac_mode)
- if hvac_mode_value is None:
+ hvac_mode_id = self._hvac_modes.get(hvac_mode)
+ if hvac_mode_id is None:
raise ValueError(f"Received an invalid hvac mode: {hvac_mode}")
- await self.info.node.async_set_value(self._current_mode, hvac_mode_value)
+
+ if not self._current_mode:
+ # Thermostat(valve) has no support for setting a mode, so we make it a no-op
+ return
+
+ await self.info.node.async_set_value(self._current_mode, hvac_mode_id)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new target preset mode."""
diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py
index ef39a043b0e..ced8b2c68cb 100644
--- a/homeassistant/components/zwave_js/config_flow.py
+++ b/homeassistant/components/zwave_js/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow for Z-Wave JS integration."""
from __future__ import annotations
+from abc import abstractmethod
import asyncio
import logging
from typing import Any
@@ -14,12 +15,20 @@ from homeassistant import config_entries, exceptions
from homeassistant.components.hassio import is_hassio
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant, callback
-from homeassistant.data_entry_flow import AbortFlow, FlowResult
+from homeassistant.data_entry_flow import (
+ AbortFlow,
+ FlowHandler,
+ FlowManager,
+ FlowResult,
+)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from . import disconnect_client
from .addon import AddonError, AddonInfo, AddonManager, AddonState, get_addon_manager
from .const import (
CONF_ADDON_DEVICE,
+ CONF_ADDON_EMULATE_HARDWARE,
+ CONF_ADDON_LOG_LEVEL,
CONF_ADDON_NETWORK_KEY,
CONF_INTEGRATION_CREATED_ADDON,
CONF_NETWORK_KEY,
@@ -35,10 +44,38 @@ TITLE = "Z-Wave JS"
ADDON_SETUP_TIMEOUT = 5
ADDON_SETUP_TIMEOUT_ROUNDS = 4
+CONF_EMULATE_HARDWARE = "emulate_hardware"
+CONF_LOG_LEVEL = "log_level"
SERVER_VERSION_TIMEOUT = 10
+ADDON_LOG_LEVELS = {
+ "error": "Error",
+ "warn": "Warn",
+ "info": "Info",
+ "verbose": "Verbose",
+ "debug": "Debug",
+ "silly": "Silly",
+}
+ADDON_USER_INPUT_MAP = {
+ CONF_ADDON_DEVICE: CONF_USB_PATH,
+ CONF_ADDON_NETWORK_KEY: CONF_NETWORK_KEY,
+ CONF_ADDON_LOG_LEVEL: CONF_LOG_LEVEL,
+ CONF_ADDON_EMULATE_HARDWARE: CONF_EMULATE_HARDWARE,
+}
+
ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool})
-STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL, default=DEFAULT_URL): str})
+
+
+def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema:
+ """Return a schema for the manual step."""
+ default_url = user_input.get(CONF_URL, DEFAULT_URL)
+ return vol.Schema({vol.Required(CONF_URL, default=default_url): str})
+
+
+def get_on_supervisor_schema(user_input: dict[str, Any]) -> vol.Schema:
+ """Return a schema for the on Supervisor step."""
+ default_use_addon = user_input[CONF_USE_ADDON]
+ return vol.Schema({vol.Optional(CONF_USE_ADDON, default=default_use_addon): bool})
async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo:
@@ -70,135 +107,25 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio
return version_info
-class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
- """Handle a config flow for Z-Wave JS."""
-
- VERSION = 1
+class BaseZwaveJSFlow(FlowHandler):
+ """Represent the base config flow for Z-Wave JS."""
def __init__(self) -> None:
"""Set up flow instance."""
self.network_key: str | None = None
self.usb_path: str | None = None
- self.use_addon = False
self.ws_address: str | None = None
+ self.restart_addon: bool = False
# If we install the add-on we should uninstall it on entry remove.
self.integration_created_addon = False
self.install_task: asyncio.Task | None = None
self.start_task: asyncio.Task | None = None
+ self.version_info: VersionInfo | None = None
- async def async_step_user(
- self, user_input: dict[str, Any] | None = None
- ) -> FlowResult:
- """Handle the initial step."""
- if is_hassio(self.hass):
- return await self.async_step_on_supervisor()
-
- return await self.async_step_manual()
-
- async def async_step_manual(
- self, user_input: dict[str, Any] | None = None
- ) -> FlowResult:
- """Handle a manual configuration."""
- if user_input is None:
- return self.async_show_form(
- step_id="manual", data_schema=STEP_USER_DATA_SCHEMA
- )
-
- errors = {}
-
- try:
- version_info = await validate_input(self.hass, user_input)
- except InvalidInput as err:
- errors["base"] = err.error
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception("Unexpected exception")
- errors["base"] = "unknown"
- else:
- await self.async_set_unique_id(
- 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.
- self._abort_if_unique_id_configured(
- updates={
- **user_input,
- CONF_USE_ADDON: False,
- CONF_INTEGRATION_CREATED_ADDON: False,
- }
- )
- self.ws_address = user_input[CONF_URL]
- return self._async_create_entry_from_vars()
-
- return self.async_show_form(
- step_id="manual", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
- )
-
- async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult:
- """Receive configuration from add-on discovery info.
-
- This flow is triggered by the Z-Wave JS add-on.
- """
- self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}"
- try:
- version_info = await async_get_version_info(self.hass, self.ws_address)
- except CannotConnect:
- return self.async_abort(reason="cannot_connect")
-
- await self.async_set_unique_id(version_info.home_id)
- self._abort_if_unique_id_configured(updates={CONF_URL: self.ws_address})
-
- return await self.async_step_hassio_confirm()
-
- async def async_step_hassio_confirm(
- self, user_input: dict[str, Any] | None = None
- ) -> FlowResult:
- """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")
-
- @callback
- def _async_create_entry_from_vars(self) -> FlowResult:
- """Return a config entry for the flow."""
- return self.async_create_entry(
- title=TITLE,
- data={
- CONF_URL: self.ws_address,
- 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,
- },
- )
-
- async def async_step_on_supervisor(
- self, user_input: dict[str, Any] | None = None
- ) -> FlowResult:
- """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 await self.async_step_manual()
-
- self.use_addon = True
-
- addon_info = await self._async_get_addon_info()
-
- if addon_info.state == AddonState.RUNNING:
- addon_config = addon_info.options
- self.usb_path = addon_config[CONF_ADDON_DEVICE]
- self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "")
- return await self.async_step_finish_addon_setup()
-
- if addon_info.state == AddonState.NOT_RUNNING:
- return await self.async_step_configure_addon()
-
- return await self.async_step_install_addon()
+ @property
+ @abstractmethod
+ def flow_manager(self) -> FlowManager:
+ """Return the flow manager of the flow."""
async def async_step_install_addon(
self, user_input: dict[str, Any] | None = None
@@ -213,10 +140,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
await self.install_task
except AddonError as err:
+ self.install_task = None
_LOGGER.error(err)
return self.async_show_progress_done(next_step_id="install_failed")
self.integration_created_addon = True
+ self.install_task = None
return self.async_show_progress_done(next_step_id="configure_addon")
@@ -226,43 +155,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Add-on installation failed."""
return self.async_abort(reason="addon_install_failed")
- async def async_step_configure_addon(
- self, user_input: dict[str, Any] | None = None
- ) -> FlowResult:
- """Ask for config for Z-Wave JS add-on."""
- addon_info = await self._async_get_addon_info()
- addon_config = addon_info.options
-
- errors: dict[str, str] = {}
-
- 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 != addon_config:
- await self._async_set_addon_config(new_addon_config)
-
- return await self.async_step_start_addon()
-
- usb_path = addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "")
- network_key = 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="configure_addon", data_schema=data_schema, errors=errors
- )
-
async def async_step_start_addon(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -275,10 +167,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
await self.start_task
- except (CannotConnect, AddonError) as err:
+ except (CannotConnect, AddonError, AbortFlow) as err:
+ self.start_task = None
_LOGGER.error(err)
return self.async_show_progress_done(next_step_id="start_failed")
+ self.start_task = None
return self.async_show_progress_done(next_step_id="finish_addon_setup")
async def async_step_start_failed(
@@ -290,8 +184,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _async_start_addon(self) -> None:
"""Start the Z-Wave JS add-on."""
addon_manager: AddonManager = get_addon_manager(self.hass)
+ self.version_info = None
try:
- await addon_manager.async_schedule_start_addon()
+ if self.restart_addon:
+ await addon_manager.async_schedule_restart_addon()
+ else:
+ await addon_manager.async_schedule_start_addon()
# Sleep some seconds to let the add-on start properly before connecting.
for _ in range(ADDON_SETUP_TIMEOUT_ROUNDS):
await asyncio.sleep(ADDON_SETUP_TIMEOUT)
@@ -301,7 +199,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.ws_address = (
f"ws://{discovery_info['host']}:{discovery_info['port']}"
)
- await async_get_version_info(self.hass, self.ws_address)
+ self.version_info = await async_get_version_info(
+ self.hass, self.ws_address
+ )
except (AbortFlow, CannotConnect) as err:
_LOGGER.debug(
"Add-on not ready yet, waiting %s seconds: %s",
@@ -315,9 +215,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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)
+ self.flow_manager.async_configure(flow_id=self.flow_id)
)
+ @abstractmethod
+ async def async_step_configure_addon(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Ask for config for Z-Wave JS add-on."""
+
+ @abstractmethod
async def async_step_finish_addon_setup(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -326,27 +233,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
Get add-on discovery info and server version info.
Set unique id and abort if already configured.
"""
- if not self.ws_address:
- discovery_info = await self._async_get_addon_discovery_info()
- self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}"
-
- if not self.unique_id:
- try:
- version_info = await async_get_version_info(self.hass, self.ws_address)
- except CannotConnect as err:
- raise AbortFlow("cannot_connect") from err
- await self.async_set_unique_id(
- version_info.home_id, raise_on_progress=False
- )
-
- self._abort_if_unique_id_configured(
- updates={
- CONF_URL: self.ws_address,
- CONF_USB_PATH: self.usb_path,
- CONF_NETWORK_KEY: self.network_key,
- }
- )
- return self._async_create_entry_from_vars()
async def _async_get_addon_info(self) -> AddonInfo:
"""Return and cache Z-Wave JS add-on info."""
@@ -376,7 +262,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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)
+ self.flow_manager.async_configure(flow_id=self.flow_id)
)
async def _async_get_addon_discovery_info(self) -> dict:
@@ -391,6 +277,441 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return discovery_info_config
+class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Z-Wave JS."""
+
+ VERSION = 1
+
+ def __init__(self) -> None:
+ """Set up flow instance."""
+ super().__init__()
+ self.use_addon = False
+
+ @property
+ def flow_manager(self) -> config_entries.ConfigEntriesFlowManager:
+ """Return the correct flow manager."""
+ return self.hass.config_entries.flow
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(
+ config_entry: config_entries.ConfigEntry,
+ ) -> OptionsFlowHandler:
+ """Return the options flow."""
+ return OptionsFlowHandler(config_entry)
+
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Handle the initial step."""
+ if is_hassio(self.hass):
+ return await self.async_step_on_supervisor()
+
+ return await self.async_step_manual()
+
+ async def async_step_manual(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Handle a manual configuration."""
+ if user_input is None:
+ return self.async_show_form(
+ step_id="manual", data_schema=get_manual_schema({})
+ )
+
+ errors = {}
+
+ try:
+ version_info = await validate_input(self.hass, user_input)
+ except InvalidInput as err:
+ errors["base"] = err.error
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+ await self.async_set_unique_id(
+ 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.
+ self._abort_if_unique_id_configured(
+ updates={
+ **user_input,
+ CONF_USE_ADDON: False,
+ CONF_INTEGRATION_CREATED_ADDON: False,
+ }
+ )
+ self.ws_address = user_input[CONF_URL]
+ return self._async_create_entry_from_vars()
+
+ return self.async_show_form(
+ step_id="manual", data_schema=get_manual_schema(user_input), errors=errors
+ )
+
+ async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult:
+ """Receive configuration from add-on discovery info.
+
+ This flow is triggered by the Z-Wave JS add-on.
+ """
+ self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}"
+ try:
+ version_info = await async_get_version_info(self.hass, self.ws_address)
+ except CannotConnect:
+ return self.async_abort(reason="cannot_connect")
+
+ await self.async_set_unique_id(version_info.home_id)
+ self._abort_if_unique_id_configured(updates={CONF_URL: self.ws_address})
+
+ return await self.async_step_hassio_confirm()
+
+ async def async_step_hassio_confirm(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """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")
+
+ async def async_step_on_supervisor(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """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 await self.async_step_manual()
+
+ self.use_addon = True
+
+ addon_info = await self._async_get_addon_info()
+
+ if addon_info.state == AddonState.RUNNING:
+ addon_config = addon_info.options
+ self.usb_path = addon_config[CONF_ADDON_DEVICE]
+ self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "")
+ return await self.async_step_finish_addon_setup()
+
+ if addon_info.state == AddonState.NOT_RUNNING:
+ return await self.async_step_configure_addon()
+
+ return await self.async_step_install_addon()
+
+ async def async_step_configure_addon(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Ask for config for Z-Wave JS add-on."""
+ addon_info = await self._async_get_addon_info()
+ addon_config = addon_info.options
+
+ 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 = {
+ **addon_config,
+ CONF_ADDON_DEVICE: self.usb_path,
+ CONF_ADDON_NETWORK_KEY: self.network_key,
+ }
+
+ if new_addon_config != addon_config:
+ await self._async_set_addon_config(new_addon_config)
+
+ return await self.async_step_start_addon()
+
+ usb_path = addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "")
+ network_key = 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="configure_addon", data_schema=data_schema)
+
+ async def async_step_finish_addon_setup(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Prepare info needed to complete the config entry.
+
+ Get add-on discovery info and server version info.
+ Set unique id and abort if already configured.
+ """
+ if not self.ws_address:
+ discovery_info = await self._async_get_addon_discovery_info()
+ self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}"
+
+ if not self.unique_id:
+ if not self.version_info:
+ try:
+ self.version_info = await async_get_version_info(
+ self.hass, self.ws_address
+ )
+ except CannotConnect as err:
+ raise AbortFlow("cannot_connect") from err
+
+ await self.async_set_unique_id(
+ self.version_info.home_id, raise_on_progress=False
+ )
+
+ self._abort_if_unique_id_configured(
+ updates={
+ CONF_URL: self.ws_address,
+ CONF_USB_PATH: self.usb_path,
+ CONF_NETWORK_KEY: self.network_key,
+ }
+ )
+ return self._async_create_entry_from_vars()
+
+ @callback
+ def _async_create_entry_from_vars(self) -> FlowResult:
+ """Return a config entry for the flow."""
+ return self.async_create_entry(
+ title=TITLE,
+ data={
+ CONF_URL: self.ws_address,
+ 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,
+ },
+ )
+
+
+class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow):
+ """Handle an options flow for Z-Wave JS."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
+ """Set up the options flow."""
+ super().__init__()
+ self.config_entry = config_entry
+ self.original_addon_config: dict[str, Any] | None = None
+ self.revert_reason: str | None = None
+
+ @property
+ def flow_manager(self) -> config_entries.OptionsFlowManager:
+ """Return the correct flow manager."""
+ return self.hass.config_entries.options
+
+ @callback
+ def _async_update_entry(self, data: dict[str, Any]) -> None:
+ """Update the config entry with new data."""
+ self.hass.config_entries.async_update_entry(self.config_entry, data=data)
+
+ async def async_step_init(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Manage the options."""
+ if is_hassio(self.hass):
+ return await self.async_step_on_supervisor()
+
+ return await self.async_step_manual()
+
+ async def async_step_manual(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Handle a manual configuration."""
+ if user_input is None:
+ return self.async_show_form(
+ step_id="manual",
+ data_schema=get_manual_schema(
+ {CONF_URL: self.config_entry.data[CONF_URL]}
+ ),
+ )
+
+ errors = {}
+
+ try:
+ version_info = await validate_input(self.hass, user_input)
+ except InvalidInput as err:
+ errors["base"] = err.error
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+ if self.config_entry.unique_id != version_info.home_id:
+ return self.async_abort(reason="different_device")
+
+ # Make sure we disable any add-on handling
+ # if the controller is reconfigured in a manual step.
+ self._async_update_entry(
+ {
+ **self.config_entry.data,
+ **user_input,
+ CONF_USE_ADDON: False,
+ CONF_INTEGRATION_CREATED_ADDON: False,
+ }
+ )
+
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.config_entry.entry_id)
+ )
+ return self.async_create_entry(title=TITLE, data={})
+
+ return self.async_show_form(
+ step_id="manual", data_schema=get_manual_schema(user_input), errors=errors
+ )
+
+ async def async_step_on_supervisor(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Handle logic when on Supervisor host."""
+ if user_input is None:
+ return self.async_show_form(
+ step_id="on_supervisor",
+ data_schema=get_on_supervisor_schema(
+ {CONF_USE_ADDON: self.config_entry.data.get(CONF_USE_ADDON, True)}
+ ),
+ )
+ if not user_input[CONF_USE_ADDON]:
+ return await self.async_step_manual()
+
+ addon_info = await self._async_get_addon_info()
+
+ if addon_info.state == AddonState.NOT_INSTALLED:
+ return await self.async_step_install_addon()
+
+ return await self.async_step_configure_addon()
+
+ async def async_step_configure_addon(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Ask for config for Z-Wave JS add-on."""
+ addon_info = await self._async_get_addon_info()
+ addon_config = addon_info.options
+
+ 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 = {
+ **addon_config,
+ CONF_ADDON_DEVICE: self.usb_path,
+ CONF_ADDON_NETWORK_KEY: self.network_key,
+ CONF_ADDON_LOG_LEVEL: user_input[CONF_LOG_LEVEL],
+ CONF_ADDON_EMULATE_HARDWARE: user_input[CONF_EMULATE_HARDWARE],
+ }
+
+ if new_addon_config != addon_config:
+ if addon_info.state == AddonState.RUNNING:
+ self.restart_addon = True
+ # Copy the add-on config to keep the objects separate.
+ self.original_addon_config = dict(addon_config)
+ await self._async_set_addon_config(new_addon_config)
+
+ if addon_info.state == AddonState.RUNNING and not self.restart_addon:
+ return await self.async_step_finish_addon_setup()
+
+ if (
+ self.config_entry.data.get(CONF_USE_ADDON)
+ and self.config_entry.state == config_entries.ConfigEntryState.LOADED
+ ):
+ # Disconnect integration before restarting add-on.
+ await disconnect_client(self.hass, self.config_entry)
+
+ return await self.async_step_start_addon()
+
+ usb_path = addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "")
+ network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, self.network_key or "")
+ log_level = addon_config.get(CONF_ADDON_LOG_LEVEL, "info")
+ emulate_hardware = addon_config.get(CONF_ADDON_EMULATE_HARDWARE, False)
+
+ data_schema = vol.Schema(
+ {
+ vol.Required(CONF_USB_PATH, default=usb_path): str,
+ vol.Optional(CONF_NETWORK_KEY, default=network_key): str,
+ vol.Optional(CONF_LOG_LEVEL, default=log_level): vol.In(
+ ADDON_LOG_LEVELS
+ ),
+ vol.Optional(CONF_EMULATE_HARDWARE, default=emulate_hardware): bool,
+ }
+ )
+
+ return self.async_show_form(step_id="configure_addon", data_schema=data_schema)
+
+ async def async_step_start_failed(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Add-on start failed."""
+ return await self.async_revert_addon_config(reason="addon_start_failed")
+
+ async def async_step_finish_addon_setup(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Prepare info needed to complete the config entry update.
+
+ Get add-on discovery info and server version info.
+ Check for same unique id and abort if not the same unique id.
+ """
+ if self.revert_reason:
+ self.original_addon_config = None
+ reason = self.revert_reason
+ self.revert_reason = None
+ return await self.async_revert_addon_config(reason=reason)
+
+ if not self.ws_address:
+ discovery_info = await self._async_get_addon_discovery_info()
+ self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}"
+
+ if not self.version_info:
+ try:
+ self.version_info = await async_get_version_info(
+ self.hass, self.ws_address
+ )
+ except CannotConnect:
+ return await self.async_revert_addon_config(reason="cannot_connect")
+
+ if self.config_entry.unique_id != self.version_info.home_id:
+ return await self.async_revert_addon_config(reason="different_device")
+
+ self._async_update_entry(
+ {
+ **self.config_entry.data,
+ CONF_URL: self.ws_address,
+ CONF_USB_PATH: self.usb_path,
+ CONF_NETWORK_KEY: self.network_key,
+ CONF_USE_ADDON: True,
+ CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon,
+ }
+ )
+ # Always reload entry since we may have disconnected the client.
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.config_entry.entry_id)
+ )
+ return self.async_create_entry(title=TITLE, data={})
+
+ async def async_revert_addon_config(self, reason: str) -> FlowResult:
+ """Abort the options flow.
+
+ If the add-on options have been changed, revert those and restart add-on.
+ """
+ # If reverting the add-on options failed, abort immediately.
+ if self.revert_reason:
+ _LOGGER.error(
+ "Failed to revert add-on options before aborting flow, reason: %s",
+ reason,
+ )
+
+ if self.revert_reason or not self.original_addon_config:
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.config_entry.entry_id)
+ )
+ return self.async_abort(reason=reason)
+
+ self.revert_reason = reason
+ addon_config_input = {
+ ADDON_USER_INPUT_MAP[addon_key]: addon_val
+ for addon_key, addon_val in self.original_addon_config.items()
+ }
+ _LOGGER.debug("Reverting add-on options, reason: %s", reason)
+ return await self.async_step_configure_addon(addon_config_input)
+
+
class CannotConnect(exceptions.HomeAssistantError):
"""Indicate connection error."""
diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py
index 629cd222bd4..9e6e37b4ee7 100644
--- a/homeassistant/components/zwave_js/const.py
+++ b/homeassistant/components/zwave_js/const.py
@@ -2,6 +2,8 @@
import logging
CONF_ADDON_DEVICE = "device"
+CONF_ADDON_EMULATE_HARDWARE = "emulate_hardware"
+CONF_ADDON_LOG_LEVEL = "log_level"
CONF_ADDON_NETWORK_KEY = "network_key"
CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon"
CONF_NETWORK_KEY = "network_key"
@@ -44,6 +46,8 @@ ATTR_DATA_TYPE = "data_type"
ATTR_WAIT_FOR_RESULT = "wait_for_result"
# service constants
+ATTR_NODES = "nodes"
+
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS = "bulk_set_partial_config_parameters"
@@ -56,5 +60,10 @@ SERVICE_REFRESH_VALUE = "refresh_value"
ATTR_REFRESH_ALL_VALUES = "refresh_all_values"
SERVICE_SET_VALUE = "set_value"
+SERVICE_MULTICAST_SET_VALUE = "multicast_set_value"
+
+ATTR_BROADCAST = "broadcast"
+
+SERVICE_PING = "ping"
ADDON_SLUG = "core_zwave_js"
diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py
index 302ccd9cd32..e01f2871604 100644
--- a/homeassistant/components/zwave_js/cover.py
+++ b/homeassistant/components/zwave_js/cover.py
@@ -79,14 +79,21 @@ def percent_to_zwave_position(value: int) -> int:
class ZWaveCover(ZWaveBaseEntity, CoverEntity):
"""Representation of a Z-Wave Cover device."""
- @property
- def device_class(self) -> str | None:
- """Return the class of this device, from component DEVICE_CLASSES."""
+ def __init__(
+ self,
+ config_entry: ConfigEntry,
+ client: ZwaveClient,
+ info: ZwaveDiscoveryInfo,
+ ) -> None:
+ """Initialize a ZWaveCover entity."""
+ super().__init__(config_entry, client, info)
+
+ # Entity class attributes
+ self._attr_device_class = DEVICE_CLASS_WINDOW
if self.info.platform_hint == "window_shutter":
- return DEVICE_CLASS_SHUTTER
+ self._attr_device_class = DEVICE_CLASS_SHUTTER
if self.info.platform_hint == "window_blind":
- return DEVICE_CLASS_BLIND
- return DEVICE_CLASS_WINDOW
+ self._attr_device_class = DEVICE_CLASS_BLIND
@property
def is_closed(self) -> bool | None:
@@ -134,6 +141,9 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity):
"""Representation of a Z-Wave motorized barrier device."""
+ _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
+ _attr_device_class = DEVICE_CLASS_GARAGE
+
def __init__(
self,
config_entry: ConfigEntry,
@@ -146,16 +156,6 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity):
"targetState", add_to_watched_value_ids=False
)
- @property
- def supported_features(self) -> int | None:
- """Flag supported features."""
- return SUPPORT_OPEN | SUPPORT_CLOSE
-
- @property
- def device_class(self) -> str | None:
- """Return the class of this device, from component DEVICE_CLASSES."""
- return DEVICE_CLASS_GARAGE
-
@property
def is_opening(self) -> bool | None:
"""Return if the cover is opening or not."""
diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py
index 2d7dc961e68..548796911af 100644
--- a/homeassistant/components/zwave_js/entity.py
+++ b/homeassistant/components/zwave_js/entity.py
@@ -9,7 +9,7 @@ from zwave_js_server.model.value import Value as ZwaveValue, get_value_id
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import DeviceInfo, Entity
+from homeassistant.helpers.entity import Entity
from .const import DOMAIN
from .discovery import ZwaveDiscoveryInfo
@@ -30,10 +30,6 @@ class ZWaveBaseEntity(Entity):
self.config_entry = config_entry
self.client = client
self.info = info
- self._name = self.generate_name()
- self._unique_id = get_unique_id(
- self.client.driver.controller.home_id, self.info.primary_value.value_id
- )
# entities requiring additional values, can add extra ids to this list
self.watched_value_ids = {self.info.primary_value.value_id}
@@ -42,6 +38,17 @@ class ZWaveBaseEntity(Entity):
self.info.additional_value_ids_to_watch
)
+ # Entity class attributes
+ self._attr_name = self.generate_name()
+ self._attr_unique_id = get_unique_id(
+ self.client.driver.controller.home_id, self.info.primary_value.value_id
+ )
+ self._attr_assumed_state = self.info.assumed_state
+ # device is precreated in main handler
+ self._attr_device_info = {
+ "identifiers": {get_device_id(self.client, self.info.node)},
+ }
+
@callback
def on_value_update(self) -> None:
"""Call when one of the watched values change.
@@ -91,14 +98,6 @@ class ZWaveBaseEntity(Entity):
)
)
- @property
- def device_info(self) -> DeviceInfo:
- """Return device information for the device registry."""
- # device is precreated in main handler
- return {
- "identifiers": {get_device_id(self.client, self.info.node)},
- }
-
def generate_name(
self,
include_value_name: bool = False,
@@ -133,16 +132,6 @@ class ZWaveBaseEntity(Entity):
return name
- @property
- def name(self) -> str:
- """Return default name from device name and value name combination."""
- return self._name
-
- @property
- def unique_id(self) -> str:
- """Return the unique_id of the entity."""
- return self._unique_id
-
@property
def available(self) -> bool:
"""Return entity availability."""
@@ -229,8 +218,3 @@ class ZWaveBaseEntity(Entity):
def should_poll(self) -> bool:
"""No polling needed."""
return False
-
- @property
- def assumed_state(self) -> bool:
- """Return True if unable to access real state of the entity."""
- return self.info.assumed_state
diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py
index beee7fefa30..81eae0fdc15 100644
--- a/homeassistant/components/zwave_js/helpers.py
+++ b/homeassistant/components/zwave_js/helpers.py
@@ -10,8 +10,14 @@ from zwave_js_server.model.value import Value as ZwaveValue
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.device_registry import async_get as async_get_dev_reg
-from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg
+from homeassistant.helpers.device_registry import (
+ DeviceRegistry,
+ async_get as async_get_dev_reg,
+)
+from homeassistant.helpers.entity_registry import (
+ EntityRegistry,
+ async_get as async_get_ent_reg,
+)
from .const import CONF_DATA_COLLECTION_OPTED_IN, DATA_CLIENT, DOMAIN
@@ -60,13 +66,17 @@ def get_home_and_node_id_from_device_id(device_id: tuple[str, ...]) -> list[str]
@callback
-def async_get_node_from_device_id(hass: HomeAssistant, device_id: str) -> ZwaveNode:
+def async_get_node_from_device_id(
+ hass: HomeAssistant, device_id: str, dev_reg: DeviceRegistry | None = None
+) -> ZwaveNode:
"""
Get node from a device ID.
Raises ValueError if device is invalid or node can't be found.
"""
- device_entry = async_get_dev_reg(hass).async_get(device_id)
+ if not dev_reg:
+ dev_reg = async_get_dev_reg(hass)
+ device_entry = dev_reg.async_get(device_id)
if not device_entry:
raise ValueError("Device ID is not valid")
@@ -111,21 +121,25 @@ def async_get_node_from_device_id(hass: HomeAssistant, device_id: str) -> ZwaveN
@callback
-def async_get_node_from_entity_id(hass: HomeAssistant, entity_id: str) -> ZwaveNode:
+def async_get_node_from_entity_id(
+ hass: HomeAssistant,
+ entity_id: str,
+ ent_reg: EntityRegistry | None = None,
+ dev_reg: DeviceRegistry | None = None,
+) -> ZwaveNode:
"""
Get node from an entity ID.
Raises ValueError if entity is invalid.
"""
- entity_entry = async_get_ent_reg(hass).async_get(entity_id)
+ if not ent_reg:
+ ent_reg = async_get_ent_reg(hass)
+ entity_entry = ent_reg.async_get(entity_id)
- if not entity_entry:
- raise ValueError("Entity ID is not valid")
-
- if entity_entry.platform != DOMAIN:
- raise ValueError("Entity is not from zwave_js integration")
+ if entity_entry is None or entity_entry.platform != DOMAIN:
+ raise ValueError(f"Entity {entity_id} is not a valid {DOMAIN} entity.")
# Assert for mypy, safe because we know that zwave_js entities are always
# tied to a device
assert entity_entry.device_id
- return async_get_node_from_device_id(hass, entity_entry.device_id)
+ return async_get_node_from_device_id(hass, entity_entry.device_id, dev_reg)
diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py
index a1ab78e6ee3..b50f2231f46 100644
--- a/homeassistant/components/zwave_js/light.py
+++ b/homeassistant/components/zwave_js/light.py
@@ -107,13 +107,10 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
value_property_key=ColorComponent.COLD_WHITE,
)
self._supported_color_modes = set()
- self._supported_features = 0
# get additional (optional) values and set features
self._target_value = self.get_zwave_value("targetValue")
self._dimming_duration = self.get_zwave_value("duration")
- if self._dimming_duration is not None:
- self._supported_features |= SUPPORT_TRANSITION
self._calculate_color_values()
if self._supports_rgbw:
self._supported_color_modes.add(COLOR_MODE_RGBW)
@@ -124,6 +121,11 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
if not self._supported_color_modes:
self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
+ # Entity class attributes
+ self._attr_supported_features = 0
+ if self._dimming_duration is not None:
+ self._attr_supported_features |= SUPPORT_TRANSITION
+
@callback
def on_value_update(self) -> None:
"""Call when a watched value is added or updated."""
@@ -179,11 +181,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
"""Flag supported features."""
return self._supported_color_modes
- @property
- def supported_features(self) -> int:
- """Flag supported features."""
- return self._supported_features
-
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
# RGB/HS color
diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json
index fd342b8d498..a48d0513c17 100644
--- a/homeassistant/components/zwave_js/manifest.json
+++ b/homeassistant/components/zwave_js/manifest.json
@@ -3,7 +3,7 @@
"name": "Z-Wave JS",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
- "requirements": ["zwave-js-server-python==0.26.1"],
+ "requirements": ["zwave-js-server-python==0.27.0"],
"codeowners": ["@home-assistant/z-wave"],
"dependencies": ["http", "websocket_api"],
"iot_class": "local_push"
diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py
index ea4b978cab5..397f7efba24 100644
--- a/homeassistant/components/zwave_js/migrate.py
+++ b/homeassistant/components/zwave_js/migrate.py
@@ -84,7 +84,11 @@ def async_migrate_old_entity(
if entry.domain != platform or entry.unique_id in registered_unique_ids:
continue
- old_ent_value_id = ValueID.from_unique_id(entry.unique_id)
+ try:
+ old_ent_value_id = ValueID.from_unique_id(entry.unique_id)
+ # Skip non value ID based unique ID's (e.g. node status sensor)
+ except IndexError:
+ continue
if value_id.is_same_value_different_endpoints(old_ent_value_id):
existing_entity_entries.append(entry)
diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py
index f427f7fac20..2a3c9820a69 100644
--- a/homeassistant/components/zwave_js/number.py
+++ b/homeassistant/components/zwave_js/number.py
@@ -46,14 +46,16 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity):
) -> None:
"""Initialize a ZwaveNumberEntity entity."""
super().__init__(config_entry, client, info)
- self._name = self.generate_name(
- include_value_name=True, alternate_value_name=info.platform_hint
- )
if self.info.primary_value.metadata.writeable:
self._target_value = self.info.primary_value
else:
self._target_value = self.get_zwave_value("targetValue")
+ # Entity class attributes
+ self._attr_name = self.generate_name(
+ include_value_name=True, alternate_value_name=info.platform_hint
+ )
+
@property
def min_value(self) -> float:
"""Return the minimum value."""
@@ -69,7 +71,7 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity):
return float(self.info.primary_value.metadata.max)
@property
- def value(self) -> float | None: # type: ignore
+ def value(self) -> float | None:
"""Return the entity value."""
if self.info.primary_value.value is None:
return None
diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py
index 40e28999a1a..064275e5729 100644
--- a/homeassistant/components/zwave_js/sensor.py
+++ b/homeassistant/components/zwave_js/sensor.py
@@ -6,6 +6,7 @@ from typing import cast
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import CommandClass, ConfigurationValueType
+from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.value import ConfigurationValue
from homeassistant.components.sensor import (
@@ -31,6 +32,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN
from .discovery import ZwaveDiscoveryInfo
from .entity import ZWaveBaseEntity
+from .helpers import get_device_id
LOGGER = logging.getLogger(__name__)
@@ -66,6 +68,11 @@ async def async_setup_entry(
async_add_entities(entities)
+ @callback
+ def async_add_node_status_sensor(node: ZwaveNode) -> None:
+ """Add node status sensor."""
+ async_add_entities([ZWaveNodeStatusSensor(config_entry, client, node)])
+
hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
async_dispatcher_connect(
hass,
@@ -74,6 +81,14 @@ async def async_setup_entry(
)
)
+ hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
+ async_dispatcher_connect(
+ hass,
+ f"{DOMAIN}_{config_entry.entry_id}_add_node_status_sensor",
+ async_add_node_status_sensor,
+ )
+ )
+
class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity):
"""Basic Representation of a Z-Wave sensor."""
@@ -86,9 +101,20 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity):
) -> None:
"""Initialize a ZWaveSensorBase entity."""
super().__init__(config_entry, client, info)
- self._name = self.generate_name(include_value_name=True)
- self._device_class = self._get_device_class()
- self._state_class = self._get_state_class()
+
+ # Entity class attributes
+ self._attr_name = self.generate_name(include_value_name=True)
+ self._attr_device_class = self._get_device_class()
+ self._attr_state_class = self._get_state_class()
+ self._attr_entity_registry_enabled_default = True
+ # We hide some of the more advanced sensors by default to not overwhelm users
+ if self.info.primary_value.command_class in [
+ CommandClass.BASIC,
+ CommandClass.CONFIGURATION,
+ CommandClass.INDICATOR,
+ CommandClass.NOTIFICATION,
+ ]:
+ self._attr_entity_registry_enabled_default = False
def _get_device_class(self) -> str | None:
"""
@@ -130,29 +156,6 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity):
return STATE_CLASS_MEASUREMENT
return None
- @property
- def device_class(self) -> str | None:
- """Return the device class of the sensor."""
- return self._device_class
-
- @property
- def state_class(self) -> str | None:
- """Return the state class of the sensor."""
- return self._state_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 some of the more advanced sensors by default to not overwhelm users
- if self.info.primary_value.command_class in [
- CommandClass.BASIC,
- CommandClass.CONFIGURATION,
- CommandClass.INDICATOR,
- CommandClass.NOTIFICATION,
- ]:
- return False
- return True
-
@property
def force_update(self) -> bool:
"""Force updates."""
@@ -188,8 +191,10 @@ class ZWaveNumericSensor(ZwaveSensorBase):
) -> None:
"""Initialize a ZWaveNumericSensor entity."""
super().__init__(config_entry, client, info)
+
+ # Entity class attributes
if self.info.primary_value.command_class == CommandClass.BASIC:
- self._name = self.generate_name(
+ self._attr_name = self.generate_name(
include_value_name=True,
alternate_value_name=self.info.primary_value.command_class_name,
)
@@ -225,7 +230,9 @@ class ZWaveListSensor(ZwaveSensorBase):
) -> None:
"""Initialize a ZWaveListSensor entity."""
super().__init__(config_entry, client, info)
- self._name = self.generate_name(
+
+ # Entity class attributes
+ self._attr_name = self.generate_name(
include_value_name=True,
alternate_value_name=self.info.primary_value.property_name,
additional_info=[self.info.primary_value.property_key_name],
@@ -263,13 +270,15 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase):
) -> None:
"""Initialize a ZWaveConfigParameterSensor entity."""
super().__init__(config_entry, client, info)
- self._name = self.generate_name(
+ self._primary_value = cast(ConfigurationValue, self.info.primary_value)
+
+ # Entity class attributes
+ self._attr_name = self.generate_name(
include_value_name=True,
alternate_value_name=self.info.primary_value.property_name,
additional_info=[self.info.primary_value.property_key_name],
name_suffix="Config Parameter",
)
- self._primary_value = cast(ConfigurationValue, self.info.primary_value)
@property
def state(self) -> str | None:
@@ -295,3 +304,61 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase):
return None
# add the value's int value as property for multi-value (list) items
return {"value": self.info.primary_value.value}
+
+
+class ZWaveNodeStatusSensor(SensorEntity):
+ """Representation of a node status sensor."""
+
+ _attr_should_poll = False
+ _attr_entity_registry_enabled_default = False
+
+ def __init__(
+ self, config_entry: ConfigEntry, client: ZwaveClient, node: ZwaveNode
+ ) -> None:
+ """Initialize a generic Z-Wave device entity."""
+ self.config_entry = config_entry
+ self.client = client
+ self.node = node
+ name: str = (
+ self.node.name
+ or self.node.device_config.description
+ or f"Node {self.node.node_id}"
+ )
+ # Entity class attributes
+ self._attr_name = f"{name}: Node Status"
+ self._attr_unique_id = (
+ f"{self.client.driver.controller.home_id}.{node.node_id}.node_status"
+ )
+ # device is precreated in main handler
+ self._attr_device_info = {
+ "identifiers": {get_device_id(self.client, self.node)},
+ }
+ self._attr_state: str = node.status.name.lower()
+
+ async def async_poll_value(self, _: bool) -> None:
+ """Poll a value."""
+ raise ValueError("There is no value to poll for this entity")
+
+ def _status_changed(self, _: dict) -> None:
+ """Call when status event is received."""
+ self._attr_state = self.node.status.name.lower()
+ self.async_write_ha_state()
+
+ async def async_added_to_hass(self) -> None:
+ """Call when entity is added."""
+ # Add value_changed callbacks.
+ for evt in ("wake up", "sleep", "dead", "alive"):
+ self.async_on_remove(self.node.on(evt, self._status_changed))
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ f"{DOMAIN}_{self.unique_id}_poll_value",
+ self.async_poll_value,
+ )
+ )
+ self.async_write_ha_state()
+
+ @property
+ def available(self) -> bool:
+ """Return entity availability."""
+ return self.client.connected and bool(self.node.ready)
diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py
index c2ebe965fdd..47709a908ed 100644
--- a/homeassistant/components/zwave_js/services.py
+++ b/homeassistant/components/zwave_js/services.py
@@ -1,13 +1,17 @@
"""Methods and classes related to executing Z-Wave commands and publishing these to hass."""
from __future__ import annotations
+import asyncio
import logging
+from typing import Any
import voluptuous as vol
+from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import CommandStatus
from zwave_js_server.exceptions import SetValueFailed
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.value import get_value_id
+from zwave_js_server.util.multicast import async_multicast_set_value
from zwave_js_server.util.node import (
async_bulk_set_partial_config_parameters,
async_set_config_parameter,
@@ -16,6 +20,7 @@ from zwave_js_server.util.node import (
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.device_registry import DeviceRegistry
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity_registry import EntityRegistry
@@ -26,8 +31,8 @@ _LOGGER = logging.getLogger(__name__)
def parameter_name_does_not_need_bitmask(
- val: dict[str, int | str]
-) -> dict[str, int | str]:
+ val: dict[str, int | str | list[str]]
+) -> dict[str, int | str | list[str]]:
"""Validate that if a parameter name is provided, bitmask is not as well."""
if isinstance(val[const.ATTR_CONFIG_PARAMETER], str) and (
val.get(const.ATTR_CONFIG_PARAMETER_BITMASK)
@@ -39,6 +44,16 @@ def parameter_name_does_not_need_bitmask(
return val
+def broadcast_command(val: dict[str, Any]) -> dict[str, Any]:
+ """Validate that the service call is for a broadcast command."""
+ if val.get(const.ATTR_BROADCAST):
+ return val
+ raise vol.Invalid(
+ "Either `broadcast` must be set to True or multiple devices/entities must be "
+ "specified"
+ )
+
+
# 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(
@@ -51,18 +66,107 @@ BITMASK_SCHEMA = vol.All(
lambda value: int(value, 16),
)
+VALUE_SCHEMA = vol.Any(
+ bool,
+ vol.Coerce(int),
+ vol.Coerce(float),
+ BITMASK_SCHEMA,
+ cv.string,
+)
+
class ZWaveServices:
"""Class that holds our services (Zwave Commands) that should be published to hass."""
- def __init__(self, hass: HomeAssistant, ent_reg: EntityRegistry) -> None:
+ def __init__(
+ self, hass: HomeAssistant, ent_reg: EntityRegistry, dev_reg: DeviceRegistry
+ ) -> None:
"""Initialize with hass object."""
self._hass = hass
self._ent_reg = ent_reg
+ self._dev_reg = dev_reg
@callback
def async_register(self) -> None:
"""Register all our services."""
+
+ @callback
+ def get_nodes_from_service_data(val: dict[str, Any]) -> dict[str, Any]:
+ """Get nodes set from service data."""
+ nodes: set[ZwaveNode] = set()
+ try:
+ if ATTR_ENTITY_ID in val:
+ nodes |= {
+ async_get_node_from_entity_id(
+ self._hass, entity_id, self._ent_reg, self._dev_reg
+ )
+ for entity_id in val[ATTR_ENTITY_ID]
+ }
+ val.pop(ATTR_ENTITY_ID)
+ if ATTR_DEVICE_ID in val:
+ nodes |= {
+ async_get_node_from_device_id(
+ self._hass, device_id, self._dev_reg
+ )
+ for device_id in val[ATTR_DEVICE_ID]
+ }
+ val.pop(ATTR_DEVICE_ID)
+ except ValueError as err:
+ raise vol.Invalid(err.args[0]) from err
+
+ val[const.ATTR_NODES] = nodes
+ return val
+
+ @callback
+ def validate_multicast_nodes(val: dict[str, Any]) -> dict[str, Any]:
+ """Validate the input nodes for multicast."""
+ nodes: set[ZwaveNode] = val[const.ATTR_NODES]
+ broadcast: bool = val[const.ATTR_BROADCAST]
+
+ # User must specify a node if they are attempting a broadcast and have more
+ # than one zwave-js network. We know it's a broadcast if the nodes list is
+ # empty because of schema validation.
+ if (
+ not nodes
+ and len(self._hass.config_entries.async_entries(const.DOMAIN)) > 1
+ ):
+ raise vol.Invalid(
+ "You must include at least one entity or device in the service call"
+ )
+
+ # When multicasting, user must specify at least two nodes
+ if not broadcast and len(nodes) < 2:
+ raise vol.Invalid(
+ "To set a value on a single node, use the zwave_js.set_value service"
+ )
+
+ first_node = next((node for node in nodes), None)
+
+ # If any nodes don't have matching home IDs, we can't run the command because
+ # we can't multicast across multiple networks
+ if first_node and any(
+ node.client.driver.controller.home_id
+ != first_node.client.driver.controller.home_id
+ for node in nodes
+ ):
+ raise vol.Invalid(
+ "Multicast commands only work on devices in the same network"
+ )
+
+ return val
+
+ @callback
+ def validate_entities(val: dict[str, Any]) -> dict[str, Any]:
+ """Validate entities exist and are from the zwave_js platform."""
+ 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."
+ )
+
+ return val
+
self._hass.services.async_register(
const.DOMAIN,
const.SERVICE_SET_CONFIG_PARAMETER,
@@ -81,11 +185,12 @@ class ZWaveServices:
vol.Coerce(int), BITMASK_SCHEMA
),
vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(
- vol.Coerce(int), cv.string
+ vol.Coerce(int), BITMASK_SCHEMA, cv.string
),
},
cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
parameter_name_does_not_need_bitmask,
+ get_nodes_from_service_data,
),
),
)
@@ -107,11 +212,12 @@ class ZWaveServices:
{
vol.Any(
vol.Coerce(int), BITMASK_SCHEMA, cv.string
- ): vol.Any(vol.Coerce(int), cv.string)
+ ): vol.Any(vol.Coerce(int), BITMASK_SCHEMA, cv.string)
},
),
},
cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
+ get_nodes_from_service_data,
),
),
)
@@ -121,10 +227,15 @@ class ZWaveServices:
const.SERVICE_REFRESH_VALUE,
self.async_poll_value,
schema=vol.Schema(
- {
- vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
- vol.Optional(const.ATTR_REFRESH_ALL_VALUES, default=False): bool,
- }
+ vol.All(
+ {
+ vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Optional(
+ const.ATTR_REFRESH_ALL_VALUES, default=False
+ ): cv.boolean,
+ },
+ validate_entities,
+ )
),
)
@@ -147,29 +258,68 @@ class ZWaveServices:
vol.Coerce(int), str
),
vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int),
- vol.Required(const.ATTR_VALUE): vol.Any(
- bool, vol.Coerce(int), vol.Coerce(float), cv.string
- ),
- vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool),
+ vol.Required(const.ATTR_VALUE): VALUE_SCHEMA,
+ vol.Optional(const.ATTR_WAIT_FOR_RESULT): cv.boolean,
},
cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
+ get_nodes_from_service_data,
+ ),
+ ),
+ )
+
+ self._hass.services.async_register(
+ const.DOMAIN,
+ const.SERVICE_MULTICAST_SET_VALUE,
+ self.async_multicast_set_value,
+ schema=vol.Schema(
+ vol.All(
+ {
+ vol.Optional(ATTR_DEVICE_ID): vol.All(
+ cv.ensure_list, [cv.string]
+ ),
+ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Optional(const.ATTR_BROADCAST, default=False): cv.boolean,
+ vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int),
+ vol.Required(const.ATTR_PROPERTY): vol.Any(
+ vol.Coerce(int), str
+ ),
+ vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any(
+ vol.Coerce(int), str
+ ),
+ vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int),
+ vol.Required(const.ATTR_VALUE): VALUE_SCHEMA,
+ },
+ vol.Any(
+ cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
+ broadcast_command,
+ ),
+ get_nodes_from_service_data,
+ validate_multicast_nodes,
+ ),
+ ),
+ )
+
+ self._hass.services.async_register(
+ const.DOMAIN,
+ const.SERVICE_PING,
+ self.async_ping,
+ schema=vol.Schema(
+ vol.All(
+ {
+ vol.Optional(ATTR_DEVICE_ID): vol.All(
+ cv.ensure_list, [cv.string]
+ ),
+ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+ },
+ cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
+ get_nodes_from_service_data,
),
),
)
async def async_set_config_parameter(self, service: ServiceCall) -> None:
"""Set a config value on a node."""
- nodes: set[ZwaveNode] = set()
- if ATTR_ENTITY_ID in service.data:
- nodes |= {
- async_get_node_from_entity_id(self._hass, entity_id)
- for entity_id in service.data[ATTR_ENTITY_ID]
- }
- if ATTR_DEVICE_ID in service.data:
- nodes |= {
- async_get_node_from_device_id(self._hass, device_id)
- for device_id in service.data[ATTR_DEVICE_ID]
- }
+ nodes = service.data[const.ATTR_NODES]
property_or_property_name = service.data[const.ATTR_CONFIG_PARAMETER]
property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK)
new_value = service.data[const.ATTR_CONFIG_VALUE]
@@ -196,17 +346,7 @@ class ZWaveServices:
self, service: ServiceCall
) -> None:
"""Bulk set multiple partial config values on a node."""
- nodes: set[ZwaveNode] = set()
- if ATTR_ENTITY_ID in service.data:
- nodes |= {
- async_get_node_from_entity_id(self._hass, entity_id)
- for entity_id in service.data[ATTR_ENTITY_ID]
- }
- if ATTR_DEVICE_ID in service.data:
- nodes |= {
- async_get_node_from_device_id(self._hass, device_id)
- for device_id in service.data[ATTR_DEVICE_ID]
- }
+ nodes = service.data[const.ATTR_NODES]
property_ = service.data[const.ATTR_CONFIG_PARAMETER]
new_value = service.data[const.ATTR_CONFIG_VALUE]
@@ -231,10 +371,7 @@ class ZWaveServices:
"""Poll value on a node."""
for entity_id in service.data[ATTR_ENTITY_ID]:
entry = self._ent_reg.async_get(entity_id)
- if entry is None or entry.platform != const.DOMAIN:
- raise ValueError(
- f"Entity {entity_id} is not a valid {const.DOMAIN} entity."
- )
+ assert entry # Schema validation would have failed if we can't do this
async_dispatcher_send(
self._hass,
f"{const.DOMAIN}_{entry.unique_id}_poll_value",
@@ -243,17 +380,7 @@ class ZWaveServices:
async def async_set_value(self, service: ServiceCall) -> None:
"""Set a value on a node."""
- nodes: set[ZwaveNode] = set()
- if ATTR_ENTITY_ID in service.data:
- nodes |= {
- async_get_node_from_entity_id(self._hass, entity_id)
- for entity_id in service.data[ATTR_ENTITY_ID]
- }
- if ATTR_DEVICE_ID in service.data:
- nodes |= {
- async_get_node_from_device_id(self._hass, device_id)
- for device_id in service.data[ATTR_DEVICE_ID]
- }
+ nodes = service.data[const.ATTR_NODES]
command_class = service.data[const.ATTR_COMMAND_CLASS]
property_ = service.data[const.ATTR_PROPERTY]
property_key = service.data.get(const.ATTR_PROPERTY_KEY)
@@ -280,3 +407,42 @@ class ZWaveServices:
"https://zwave-js.github.io/node-zwave-js/#/api/node?id=setvalue "
"for possible reasons"
)
+
+ async def async_multicast_set_value(self, service: ServiceCall) -> None:
+ """Set a value via multicast to multiple nodes."""
+ nodes = service.data[const.ATTR_NODES]
+ broadcast: bool = service.data[const.ATTR_BROADCAST]
+
+ value = {
+ "commandClass": service.data[const.ATTR_COMMAND_CLASS],
+ "property": service.data[const.ATTR_PROPERTY],
+ "propertyKey": service.data.get(const.ATTR_PROPERTY_KEY),
+ "endpoint": service.data.get(const.ATTR_ENDPOINT),
+ }
+ new_value = service.data[const.ATTR_VALUE]
+
+ # If there are no nodes, we can assume there is only one config entry due to
+ # schema validation and can use that to get the client, otherwise we can just
+ # get the client from the node.
+ client: ZwaveClient = None
+ first_node = next((node for node in nodes), None)
+ if first_node:
+ client = first_node.client
+ else:
+ entry_id = self._hass.config_entries.async_entries(const.DOMAIN)[0].entry_id
+ client = self._hass.data[const.DOMAIN][entry_id][const.DATA_CLIENT]
+
+ success = await async_multicast_set_value(
+ client,
+ new_value,
+ {k: v for k, v in value.items() if v is not None},
+ None if broadcast else list(nodes),
+ )
+
+ if success is False:
+ raise SetValueFailed("Unable to set value via multicast")
+
+ async def async_ping(self, service: ServiceCall) -> None:
+ """Ping node(s)."""
+ nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
+ await asyncio.gather(*[node.async_ping() for node in nodes])
diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml
index 84877189298..c24fa4694cf 100644
--- a/homeassistant/components/zwave_js/services.yaml
+++ b/homeassistant/components/zwave_js/services.yaml
@@ -160,3 +160,60 @@ set_value:
required: false
selector:
boolean:
+
+multicast_set_value:
+ name: Set a value on multiple Z-Wave devices via multicast (Advanced)
+ description: Allow for changing any value that Z-Wave JS recognizes on multiple Z-Wave devices using multicast, so all devices receive the message simultaneously. This service has minimal validation so only use this service if you know what you are doing.
+ target:
+ entity:
+ integration: zwave_js
+ fields:
+ broadcast:
+ name: Broadcast?
+ description: Whether command should be broadcast to all devices on the networrk.
+ example: true
+ required: false
+ selector:
+ boolean:
+ command_class:
+ name: Command Class
+ description: The ID of the command class for the value.
+ example: 117
+ required: true
+ selector:
+ text:
+ endpoint:
+ name: Endpoint
+ description: The endpoint for the value.
+ example: 1
+ required: false
+ selector:
+ text:
+ property:
+ name: Property
+ description: The ID of the property for the value.
+ example: currentValue
+ required: true
+ selector:
+ text:
+ property_key:
+ name: Property Key
+ description: The ID of the property key for the value
+ example: 1
+ required: false
+ selector:
+ text:
+ value:
+ name: Value
+ description: The new value to set.
+ example: "ffbb99"
+ required: true
+ selector:
+ object:
+
+ping:
+ name: Ping a node
+ description: Forces Z-Wave JS to try to reach a node. This can be used to update the status of the node in Z-Wave JS when you think it doesn't accurately reflect reality, e.g. reviving a failed/dead node or marking the node as asleep.
+ target:
+ entity:
+ integration: zwave_js
diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json
index eb13ad512e3..b942a75b27a 100644
--- a/homeassistant/components/zwave_js/strings.json
+++ b/homeassistant/components/zwave_js/strings.json
@@ -47,5 +47,51 @@
"install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.",
"start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds."
}
+ },
+ "options": {
+ "step": {
+ "manual": {
+ "data": {
+ "url": "[%key:common::config_flow::data::url%]"
+ }
+ },
+ "on_supervisor": {
+ "title": "Select connection method",
+ "description": "Do you want to use the Z-Wave JS Supervisor add-on?",
+ "data": { "use_addon": "Use the Z-Wave JS Supervisor add-on" }
+ },
+ "install_addon": {
+ "title": "The Z-Wave JS add-on installation has started"
+ },
+ "configure_addon": {
+ "title": "Enter the Z-Wave JS add-on configuration",
+ "data": {
+ "usb_path": "[%key:common::config_flow::data::usb_path%]",
+ "network_key": "Network Key",
+ "log_level": "Log level",
+ "emulate_hardware": "Emulate Hardware"
+ }
+ },
+ "start_addon": { "title": "The Z-Wave JS add-on is starting." }
+ },
+ "error": {
+ "invalid_ws_url": "Invalid websocket URL",
+ "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%]",
+ "addon_info_failed": "Failed to get Z-Wave JS add-on info.",
+ "addon_install_failed": "Failed to install the Z-Wave JS add-on.",
+ "addon_set_config_failed": "Failed to set Z-Wave JS configuration.",
+ "addon_start_failed": "Failed to start the Z-Wave JS add-on.",
+ "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "different_device": "The connected USB device is not the same as previously configured for this config entry. Please instead create a new config entry for the new device."
+ },
+ "progress": {
+ "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.",
+ "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds."
+ }
}
}
diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py
index 0be5d1d7f61..1fb5480f2a1 100644
--- a/homeassistant/components/zwave_js/switch.py
+++ b/homeassistant/components/zwave_js/switch.py
@@ -88,21 +88,18 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity):
) -> None:
"""Initialize a ZWaveBarrierEventSignalingSwitch entity."""
super().__init__(config_entry, client, info)
- self._name = self.generate_name(include_value_name=True)
self._state: bool | None = None
self._update_state()
+ # Entity class attributes
+ self._attr_name = self.generate_name(include_value_name=True)
+
@callback
def on_value_update(self) -> None:
"""Call when a watched value is added or updated."""
self._update_state()
- @property
- def name(self) -> str:
- """Return default name from device name and value name combination."""
- return self._name
-
@property
def is_on(self) -> bool | None: # type: ignore
"""Return a boolean for the state of the switch."""
diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json
index af39281c06b..d487ea8b902 100644
--- a/homeassistant/components/zwave_js/translations/ca.json
+++ b/homeassistant/components/zwave_js/translations/ca.json
@@ -51,5 +51,55 @@
}
}
},
+ "options": {
+ "abort": {
+ "addon_get_discovery_info_failed": "No s'ha pogut obtenir la informaci\u00f3 de descobriment del complement Z-Wave JS.",
+ "addon_info_failed": "No s'ha pogut obtenir la informaci\u00f3 del complement Z-Wave JS.",
+ "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement Z-Wave JS.",
+ "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 de Z-Wave JS.",
+ "addon_start_failed": "No s'ha pogut iniciar el complement Z-Wave JS.",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "different_device": "El dispositiu USB connectat per a aquesta entrada de configuraci\u00f3 no \u00e9s el mateix que el configurat anteriorment. Crea na entrada de configuraci\u00f3 nova per al dispositiu nou."
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_ws_url": "URL websocket inv\u00e0lid",
+ "unknown": "Error inesperat"
+ },
+ "progress": {
+ "install_addon": "Espera mentre finalitza la instal\u00b7laci\u00f3 del complement Z-Wave JS. Pot tardar uns quants minuts.",
+ "start_addon": "Espera mentre es completa la inicialitzaci\u00f3 del complement Z-Wave JS. Pot tardar uns segons."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "emulate_hardware": "Emula maquinari",
+ "log_level": "Nivell dels registres",
+ "network_key": "Clau de xarxa",
+ "usb_path": "Ruta del port USB del dispositiu"
+ },
+ "title": "Introdueix la configuraci\u00f3 del complement Z-Wave JS"
+ },
+ "install_addon": {
+ "title": "Ha comen\u00e7at la instal\u00b7laci\u00f3 del complement Z-Wave JS"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Utilitza el complement Z-Wave JS Supervisor"
+ },
+ "description": "Vols utilitzar el complement Supervisor de Z-Wave JS?",
+ "title": "Selecciona el m\u00e8tode de connexi\u00f3"
+ },
+ "start_addon": {
+ "title": "El complement Z-Wave JS s'est\u00e0 iniciant."
+ }
+ }
+ },
"title": "Z-Wave JS"
}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json
index c4672112fe5..a5c637b51fa 100644
--- a/homeassistant/components/zwave_js/translations/de.json
+++ b/homeassistant/components/zwave_js/translations/de.json
@@ -51,5 +51,55 @@
}
}
},
+ "options": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Die Discovery-Informationen des Z-Wave JS-Add-On konnten nicht abgerufen werden.",
+ "addon_info_failed": "Die Informationen des Z-Wave JS-Add-On konnten nicht abgerufen werden.",
+ "addon_install_failed": "Die Installation des Z-Wave-JS-Add-ons ist fehlgeschlagen.",
+ "addon_set_config_failed": "Fehler beim Festlegen der Z-Wave JS-Konfiguration.",
+ "addon_start_failed": "Der Start des Z-Wave JS-Add-ons ist fehlgeschlagen.",
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "different_device": "Das angeschlossene USB-Ger\u00e4t ist nicht dasselbe, das zuvor f\u00fcr diesen Config-Eintrag konfiguriert wurde. Bitte erstelle stattdessen einen neuen Konfigurationseintrag f\u00fcr das neue Ger\u00e4t."
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_ws_url": "Ung\u00fcltige Websocket-URL",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "progress": {
+ "install_addon": "Bitte warte, w\u00e4hrend die Installation des Z-Wave JS-Add-ons abgeschlossen wird. Dies kann einige Minuten dauern.",
+ "start_addon": "Bitte warte, w\u00e4hrend der Start des Z-Wave JS-Add-ons abgeschlossen wird. Dies kann einige Sekunden dauern."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "emulate_hardware": "Hardware emulieren",
+ "log_level": "Protokollstufe",
+ "network_key": "Netzwerkschl\u00fcssel",
+ "usb_path": "USB-Ger\u00e4te-Pfad"
+ },
+ "title": "Gib die Konfiguration des Z-Wave JS-Add-ons ein"
+ },
+ "install_addon": {
+ "title": "Die Installation des Z-Wave-JS-Add-ons hat begonnen"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Verwende das Supervisor Add-on Z-Wave JS"
+ },
+ "description": "M\u00f6chtest du das Supervisor Add-on Z-Wave JS verwenden?",
+ "title": "Verbindungsmethode w\u00e4hlen"
+ },
+ "start_addon": {
+ "title": "Das Z-Wave JS Add-on wird gestartet."
+ }
+ }
+ },
"title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json
index 101942dc717..27cafb6af6e 100644
--- a/homeassistant/components/zwave_js/translations/en.json
+++ b/homeassistant/components/zwave_js/translations/en.json
@@ -51,5 +51,55 @@
}
}
},
+ "options": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.",
+ "addon_info_failed": "Failed to get Z-Wave JS add-on info.",
+ "addon_install_failed": "Failed to install the Z-Wave JS add-on.",
+ "addon_set_config_failed": "Failed to set Z-Wave JS configuration.",
+ "addon_start_failed": "Failed to start the Z-Wave JS add-on.",
+ "already_configured": "Device is already configured",
+ "cannot_connect": "Failed to connect",
+ "different_device": "The connected USB device is not the same as previously configured for this config entry. Please instead create a new config entry for the new device."
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_ws_url": "Invalid websocket URL",
+ "unknown": "Unexpected error"
+ },
+ "progress": {
+ "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.",
+ "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "emulate_hardware": "Emulate Hardware",
+ "log_level": "Log level",
+ "network_key": "Network Key",
+ "usb_path": "USB Device Path"
+ },
+ "title": "Enter the Z-Wave JS add-on configuration"
+ },
+ "install_addon": {
+ "title": "The Z-Wave JS add-on installation has started"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Use the Z-Wave JS Supervisor add-on"
+ },
+ "description": "Do you want to use the Z-Wave JS Supervisor add-on?",
+ "title": "Select connection method"
+ },
+ "start_addon": {
+ "title": "The Z-Wave JS add-on is starting."
+ }
+ }
+ },
"title": "Z-Wave JS"
}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json
index 2ae0a0f47c6..434a39e61d7 100644
--- a/homeassistant/components/zwave_js/translations/et.json
+++ b/homeassistant/components/zwave_js/translations/et.json
@@ -51,5 +51,55 @@
}
}
},
+ "options": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Z-Wave JS lisandmooduli tuvastusteabe hankimine nurjus.",
+ "addon_info_failed": "Z-Wave JS lisandmooduli teabe hankimine nurjus.",
+ "addon_install_failed": "Z-Wave JS lisandmooduli paigaldamine nurjus.",
+ "addon_set_config_failed": "Z-Wave JS konfiguratsiooni m\u00e4\u00e4ramine nurjus.",
+ "addon_start_failed": "Z-Wave JS-i lisandmooduli k\u00e4ivitamine nurjus.",
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "different_device": "\u00dchendatud USB-seade ei ole sama, mis on varem selle seadekande jaoks seadistatud. Selle asemel loo uue seadme jaoks uus seadekanne."
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_ws_url": "Vale sihtkoha aadress",
+ "unknown": "Tundmatu t\u00f5rge"
+ },
+ "progress": {
+ "install_addon": "Palun oota kuni Z-Wave JS lisandmoodul on paigaldatud. See v\u00f5ib v\u00f5tta mitu minutit.",
+ "start_addon": "Palun oota kuni Z-Wave JS lisandmooduli ak\u00e4ivitumine l\u00f5ppeb. See v\u00f5ib v\u00f5tta m\u00f5ned sekundid."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "emulate_hardware": "Riistvara emuleerimine",
+ "log_level": "Logimise tase",
+ "network_key": "V\u00f5rgu v\u00f5ti",
+ "usb_path": "USB-seadme asukoha rada"
+ },
+ "title": "Sisesta Z-Wave JS lisandmooduli seaded"
+ },
+ "install_addon": {
+ "title": "Z-Wave JS lisandmooduli paigaldamine on alanud"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Kasuta Z-Wave JS Supervisori lisandmoodulit"
+ },
+ "description": "Kas soovid kasutada Z-Wave JSi halduri lisandmoodulit?",
+ "title": "Vali \u00fchendusviis"
+ },
+ "start_addon": {
+ "title": "Z-Wave JS lisandmoodul k\u00e4ivitub."
+ }
+ }
+ },
"title": "Z-Wave JS"
}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/he.json b/homeassistant/components/zwave_js/translations/he.json
new file mode 100644
index 00000000000..4dbfc33457f
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/he.json
@@ -0,0 +1,32 @@
+{
+ "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",
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB"
+ }
+ },
+ "manual": {
+ "data": {
+ "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 \u05de\u05e4\u05e7\u05d7 Z-Wave JS"
+ },
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 \u05de\u05e4\u05e7\u05d7 Z-Wave JS?",
+ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json
index 87629666b09..484dbfa1824 100644
--- a/homeassistant/components/zwave_js/translations/hu.json
+++ b/homeassistant/components/zwave_js/translations/hu.json
@@ -40,5 +40,18 @@
}
}
},
+ "options": {
+ "step": {
+ "configure_addon": {
+ "data": {
+ "log_level": "Napl\u00f3szint",
+ "network_key": "H\u00e1l\u00f3zati kulcs"
+ }
+ },
+ "on_supervisor": {
+ "title": "V\u00e1laszd ki a csatlakoz\u00e1si m\u00f3dot"
+ }
+ }
+ },
"title": "Z-Wave JS"
}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json
index f3005fc1651..71832e4882b 100644
--- a/homeassistant/components/zwave_js/translations/it.json
+++ b/homeassistant/components/zwave_js/translations/it.json
@@ -51,5 +51,55 @@
}
}
},
+ "options": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Impossibile ottenere le informazioni sul rilevamento del componente aggiuntivo Z-Wave JS.",
+ "addon_info_failed": "Impossibile ottenere le informazioni sul componente aggiuntivo Z-Wave JS.",
+ "addon_install_failed": "Impossibile installare il componente aggiuntivo Z-Wave JS.",
+ "addon_set_config_failed": "Impossibile impostare la configurazione di Z-Wave JS.",
+ "addon_start_failed": "Impossibile avviare il componente aggiuntivo Z-Wave JS.",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi",
+ "different_device": "Il dispositivo USB connesso non \u00e8 lo stesso configurato in precedenza per questa voce di configurazione. Si prega, invece, di creare una nuova voce di configurazione per il nuovo dispositivo."
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_ws_url": "URL del websocket non valido",
+ "unknown": "Errore imprevisto"
+ },
+ "progress": {
+ "install_addon": "Attendere il termine dell'installazione del componente aggiuntivo Z-Wave JS. Questo pu\u00f2 richiedere diversi minuti.",
+ "start_addon": "Attendere il completamento dell'avvio del componente aggiuntivo Z-Wave JS. Questa operazione potrebbe richiedere alcuni secondi."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "emulate_hardware": "Emulare l'hardware",
+ "log_level": "Livello di registro",
+ "network_key": "Chiave di rete",
+ "usb_path": "Percorso del dispositivo USB"
+ },
+ "title": "Entra nella configurazione del componente aggiuntivo Z-Wave JS"
+ },
+ "install_addon": {
+ "title": "L'installazione del componente aggiuntivo Z-Wave JS \u00e8 iniziata"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Usa il componente aggiuntivo Z-Wave JS di Supervisor"
+ },
+ "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS di Supervisor?",
+ "title": "Seleziona il metodo di connessione"
+ },
+ "start_addon": {
+ "title": "Il componente aggiuntivo Z-Wave JS si sta avviando."
+ }
+ }
+ },
"title": "Z-Wave JS"
}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json
new file mode 100644
index 00000000000..f7f882aa078
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/ja.json
@@ -0,0 +1,26 @@
+{
+ "options": {
+ "step": {
+ "configure_addon": {
+ "data": {
+ "log_level": "\u30ed\u30b0\u30ec\u30d9\u30eb",
+ "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af"
+ },
+ "title": "Z-Wave JS\u306e\u30a2\u30c9\u30aa\u30f3\u304c\u59cb\u307e\u308a\u307e\u3059\u3002"
+ },
+ "install_addon": {
+ "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u958b\u59cb\u3055\u308c\u307e\u3057\u305f\u3002"
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "\u30a2\u30c9\u30aa\u30f3\u300cZ-Wave JS Supervisor\u300d\u306e\u4f7f\u7528"
+ },
+ "description": "Z-Wave JS Supervisor\u30a2\u30c9\u30aa\u30f3\u3092\u4f7f\u7528\u3057\u307e\u3059\u304b\uff1f",
+ "title": "\u63a5\u7d9a\u65b9\u6cd5\u306e\u9078\u629e"
+ },
+ "start_addon": {
+ "title": "Z-Wave JS\u306e\u30a2\u30c9\u30aa\u30f3\u304c\u59cb\u307e\u308a\u307e\u3059\u3002"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json
index 090733da15b..1e37b617c6d 100644
--- a/homeassistant/components/zwave_js/translations/nl.json
+++ b/homeassistant/components/zwave_js/translations/nl.json
@@ -51,5 +51,54 @@
}
}
},
+ "options": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Ophalen van ontdekkingsinformatie voor Z-Wave JS-add-on is mislukt.",
+ "addon_info_failed": "Ophalen van Z-Wave JS add-on-info is mislukt.",
+ "addon_install_failed": "Kan de Z-Wave JS add-on niet installeren.",
+ "addon_set_config_failed": "Instellen van de Z-Wave JS configuratie is mislukt.",
+ "addon_start_failed": "Kan de Z-Wave JS add-on niet starten.",
+ "already_configured": "Apparaat is al geconfigureerd",
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_ws_url": "Ongeldige websocket URL",
+ "unknown": "Onverwachte fout"
+ },
+ "progress": {
+ "install_addon": "Wacht alstublieft terwijl de Z-Wave JS add-on installatie voltooid is. Dit kan enkele minuten duren.",
+ "start_addon": "Wacht alstublieft terwijl de Z-Wave JS add-on start voltooid is. Dit kan enkele seconden duren."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "emulate_hardware": "Emulate Hardware",
+ "log_level": "Log level",
+ "network_key": "Netwerksleutel",
+ "usb_path": "USB-apparaatpad"
+ },
+ "title": "Voer de configuratie van de Z-Wave JS-add-on in"
+ },
+ "install_addon": {
+ "title": "De Z-Wave JS add-on installatie is gestart"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Gebruik de Z-Wave JS Supervisor add-on"
+ },
+ "description": "Wilt u de Z-Wave JS Supervisor add-on gebruiken?",
+ "title": "Selecteer een verbindingsmethode"
+ },
+ "start_addon": {
+ "title": "The Z-Wave JS add-on is aan het starten."
+ }
+ }
+ },
"title": "Z-Wave JS"
}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json
index aa0fa2451aa..8eb4c176356 100644
--- a/homeassistant/components/zwave_js/translations/no.json
+++ b/homeassistant/components/zwave_js/translations/no.json
@@ -51,5 +51,55 @@
}
}
},
+ "options": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Kunne ikke hente oppdagelsesinformasjon om Z-Wave JS-tillegg",
+ "addon_info_failed": "Kunne ikke hente informasjon om Z-Wave JS-tillegg",
+ "addon_install_failed": "Kunne ikke installere Z-Wave JS-tillegg",
+ "addon_set_config_failed": "Kunne ikke angi Z-Wave JS-konfigurasjon",
+ "addon_start_failed": "Kunne ikke starte Z-Wave JS-tillegget.",
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes",
+ "different_device": "Den tilkoblede USB-enheten er ikke den samme som tidligere konfigurert for denne konfigurasjonsoppf\u00f8ringen. Opprett i stedet en ny konfigurasjonsoppf\u00f8ring for den nye enheten."
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_ws_url": "Ugyldig URL-adresse for websocket",
+ "unknown": "Uventet feil"
+ },
+ "progress": {
+ "install_addon": "Vent mens installasjonen av Z-Wave JS-tillegg er ferdig. Dette kan ta flere minutter.",
+ "start_addon": "Vent mens Z-Wave JS-tillegget er ferdig startet. Dette kan ta noen sekunder."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "emulate_hardware": "Emuler maskinvare",
+ "log_level": "Loggniv\u00e5",
+ "network_key": "Nettverksn\u00f8kkel",
+ "usb_path": "USB enhetsbane"
+ },
+ "title": "Angi konfigurasjon for Z-Wave JS-tillegg"
+ },
+ "install_addon": {
+ "title": "Installasjon av Z-Wave JS-tillegg har startet"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Bruk Z-Wave JS Supervisor-tillegg"
+ },
+ "description": "Vil du bruke Z-Wave JS Supervisor-tillegg?",
+ "title": "Velg tilkoblingsmetode"
+ },
+ "start_addon": {
+ "title": "Z-Wave JS-tillegget starter"
+ }
+ }
+ },
"title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json
index b0b3745fac4..64c8101740b 100644
--- a/homeassistant/components/zwave_js/translations/ru.json
+++ b/homeassistant/components/zwave_js/translations/ru.json
@@ -41,7 +41,57 @@
},
"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 Z-Wave JS"
+ "use_addon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS"
+ },
+ "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 Z-Wave JS?",
+ "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": {
+ "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f"
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "addon_get_discovery_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\u0431 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS.",
+ "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 Z-Wave JS.",
+ "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.",
+ "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 Z-Wave JS.",
+ "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.",
+ "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.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "different_device": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0435 USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u0440\u0430\u043d\u0435\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u043e\u0433\u043e \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438. \u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043d\u043e\u0432\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_ws_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "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 Z-Wave JS. \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.",
+ "start_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u043a \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "emulate_hardware": "\u042d\u043c\u0443\u043b\u044f\u0446\u0438\u044f \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u044f",
+ "log_level": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c \u0436\u0443\u0440\u043d\u0430\u043b\u0430",
+ "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 Z-Wave JS"
+ },
+ "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 Z-Wave JS"
+ },
+ "manual": {
+ "data": {
+ "url": "URL-\u0430\u0434\u0440\u0435\u0441"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS"
},
"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 Z-Wave JS?",
"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"
diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json
index 827c0b54e90..3e43c500c39 100644
--- a/homeassistant/components/zwave_js/translations/zh-Hant.json
+++ b/homeassistant/components/zwave_js/translations/zh-Hant.json
@@ -51,5 +51,55 @@
}
}
},
+ "options": {
+ "abort": {
+ "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\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",
+ "addon_start_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u5931\u6557\u3002",
+ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "different_device": "\u6240\u9023\u63a5\u7684 USB \u88dd\u7f6e\u8207\u4e4b\u524d\u7684\u8a2d\u5b9a\u88dd\u7f6e\u4e0d\u540c\uff0c\u8acb\u91dd\u5c0d\u65b0\u88dd\u7f6e\u65b0\u589e\u8a2d\u5b9a\u3002"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_ws_url": "Websocket URL \u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "progress": {
+ "install_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002",
+ "start_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002"
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "emulate_hardware": "\u6a21\u64ec\u786c\u9ad4",
+ "log_level": "\u65e5\u8a8c\u8a18\u9304\u7b49\u7d1a",
+ "network_key": "\u7db2\u8def\u5bc6\u9470",
+ "usb_path": "USB \u88dd\u7f6e\u8def\u5f91"
+ },
+ "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a"
+ },
+ "install_addon": {
+ "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5df2\u555f\u52d5"
+ },
+ "manual": {
+ "data": {
+ "url": "\u7db2\u5740"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "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"
+ },
+ "start_addon": {
+ "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u4e2d\u3002"
+ }
+ }
+ },
"title": "Z-Wave JS"
}
\ No newline at end of file
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 49892937217..2bb8c4f3e29 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -819,6 +819,19 @@ class ConfigEntries:
dev_reg.async_clear_config_entry(entry_id)
ent_reg.async_clear_config_entry(entry_id)
+ # If the configuration entry is removed during reauth, it should
+ # abort any reauth flow that is active for the removed entry.
+ for progress_flow in self.hass.config_entries.flow.async_progress():
+ context = progress_flow.get("context")
+ if (
+ context
+ and context["source"] == SOURCE_REAUTH
+ and "entry_id" in context
+ and context["entry_id"] == entry_id
+ and "flow_id" in progress_flow
+ ):
+ self.hass.config_entries.flow.async_abort(progress_flow["flow_id"])
+
# After we have fully removed an "ignore" config entry we can try and rediscover it so that a
# user is able to immediately start configuring it. We do this by starting a new flow with
# the 'unignore' step. If the integration doesn't implement async_step_unignore then
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 0bc06252960..27eafd0287e 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -4,8 +4,8 @@ from __future__ import annotations
from typing import Final
MAJOR_VERSION: Final = 2021
-MINOR_VERSION: Final = 6
-PATCH_VERSION: Final = "6"
+MINOR_VERSION: Final = 7
+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, 8, 0)
@@ -235,16 +235,17 @@ EVENT_TIME_CHANGED: Final = "time_changed"
DEVICE_CLASS_BATTERY: Final = "battery"
DEVICE_CLASS_CO: Final = "carbon_monoxide"
DEVICE_CLASS_CO2: Final = "carbon_dioxide"
+DEVICE_CLASS_CURRENT: Final = "current"
+DEVICE_CLASS_ENERGY: Final = "energy"
DEVICE_CLASS_HUMIDITY: Final = "humidity"
DEVICE_CLASS_ILLUMINANCE: Final = "illuminance"
+DEVICE_CLASS_MONETARY: Final = "monetary"
+DEVICE_CLASS_POWER_FACTOR: Final = "power_factor"
+DEVICE_CLASS_POWER: Final = "power"
+DEVICE_CLASS_PRESSURE: Final = "pressure"
DEVICE_CLASS_SIGNAL_STRENGTH: Final = "signal_strength"
DEVICE_CLASS_TEMPERATURE: Final = "temperature"
DEVICE_CLASS_TIMESTAMP: Final = "timestamp"
-DEVICE_CLASS_PRESSURE: Final = "pressure"
-DEVICE_CLASS_POWER: Final = "power"
-DEVICE_CLASS_CURRENT: Final = "current"
-DEVICE_CLASS_ENERGY: Final = "energy"
-DEVICE_CLASS_POWER_FACTOR: Final = "power_factor"
DEVICE_CLASS_VOLTAGE: Final = "voltage"
# #### STATES ####
@@ -493,6 +494,7 @@ PERCENTAGE: Final = "%"
# Irradiation units
IRRADIATION_WATTS_PER_SQUARE_METER: Final = "W/m²"
+IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final = "BTU/(h×ft²)"
# Precipitation units
PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h"
@@ -500,6 +502,7 @@ PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h"
# Concentration units
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³"
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³"
+CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = "μg/ft³"
CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³"
CONCENTRATION_PARTS_PER_MILLION: Final = "ppm"
CONCENTRATION_PARTS_PER_BILLION: Final = "ppb"
@@ -634,6 +637,7 @@ HTTP_BAD_GATEWAY: Final = 502
HTTP_SERVICE_UNAVAILABLE: Final = 503
HTTP_BASIC_AUTHENTICATION: Final = "basic"
+HTTP_BEARER_AUTHENTICATION: Final = "bearer_token"
HTTP_DIGEST_AUTHENTICATION: Final = "digest"
HTTP_HEADER_X_REQUESTED_WITH: Final = "X-Requested-With"
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 79245491a7e..e71503ce5fc 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -18,6 +18,7 @@ FLOWS = [
"airvisual",
"alarmdecoder",
"almond",
+ "ambee",
"ambiclimate",
"ambient_station",
"apple_tv",
@@ -44,6 +45,7 @@ FLOWS = [
"cert_expiry",
"climacell",
"cloudflare",
+ "coinbase",
"control4",
"coolmaster",
"coronavirus",
@@ -55,6 +57,7 @@ FLOWS = [
"dialogflow",
"directv",
"doorbird",
+ "dsmr",
"dunehd",
"dynalite",
"eafm",
@@ -75,9 +78,11 @@ FLOWS = [
"flo",
"flume",
"flunearyou",
+ "forecast_solar",
"forked_daapd",
"foscam",
"freebox",
+ "freedompro",
"fritz",
"fritzbox",
"fritzbox_callmonitor",
@@ -156,6 +161,7 @@ FLOWS = [
"mill",
"minecraft_server",
"mobile_app",
+ "modern_forms",
"monoprice",
"motion_blinds",
"motioneye",
@@ -287,6 +293,7 @@ FLOWS = [
"xbox",
"xiaomi_aqara",
"xiaomi_miio",
+ "yamaha_musiccast",
"yeelight",
"zerproc",
"zha",
diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py
index 0f6c01a0605..1638d932e89 100644
--- a/homeassistant/generated/ssdp.py
+++ b/homeassistant/generated/ssdp.py
@@ -215,5 +215,10 @@ SSDP = {
{
"manufacturer": "All Automacao Ltda"
}
+ ],
+ "yamaha_musiccast": [
+ {
+ "manufacturer": "Yamaha Corporation"
+ }
]
}
diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py
index 014edc4b1f3..11fd47469f8 100644
--- a/homeassistant/generated/zeroconf.py
+++ b/homeassistant/generated/zeroconf.py
@@ -60,6 +60,12 @@ ZEROCONF = {
"domain": "devolo_home_control"
}
],
+ "_easylink._tcp.local.": [
+ {
+ "domain": "modern_forms",
+ "name": "wac*"
+ }
+ ],
"_elg._tcp.local.": [
{
"domain": "elgato"
@@ -108,6 +114,10 @@ ZEROCONF = {
"domain": "nam",
"name": "nam-*"
},
+ {
+ "domain": "nam",
+ "manufacturer": "nettigo"
+ },
{
"domain": "rachio",
"name": "rachio*"
diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py
index a467d952683..6f5e7c40d22 100644
--- a/homeassistant/helpers/condition.py
+++ b/homeassistant/helpers/condition.py
@@ -16,7 +16,9 @@ from homeassistant.components import zone as zone_cmp
from homeassistant.components.device_automation import (
async_get_device_automation_platform,
)
+from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP
from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
ATTR_GPS_ACCURACY,
ATTR_LATITUDE,
ATTR_LONGITUDE,
@@ -29,6 +31,7 @@ from homeassistant.const import (
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
+ CONF_ID,
CONF_STATE,
CONF_VALUE_TEMPLATE,
CONF_WEEKDAY,
@@ -736,11 +739,24 @@ def time(
after_entity = hass.states.get(after)
if not after_entity:
raise ConditionErrorMessage("time", f"unknown 'after' entity {after}")
- after = dt_util.dt.time(
- after_entity.attributes.get("hour", 23),
- after_entity.attributes.get("minute", 59),
- after_entity.attributes.get("second", 59),
- )
+ if after_entity.domain == "input_datetime":
+ after = dt_util.dt.time(
+ after_entity.attributes.get("hour", 23),
+ after_entity.attributes.get("minute", 59),
+ after_entity.attributes.get("second", 59),
+ )
+ elif after_entity.attributes.get(
+ ATTR_DEVICE_CLASS
+ ) == DEVICE_CLASS_TIMESTAMP and after_entity.state not in (
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+ ):
+ after_datetime = dt_util.parse_datetime(after_entity.state)
+ if after_datetime is None:
+ return False
+ after = dt_util.as_local(after_datetime).time()
+ else:
+ return False
if before is None:
before = dt_util.dt.time(23, 59, 59, 999999)
@@ -748,11 +764,24 @@ def time(
before_entity = hass.states.get(before)
if not before_entity:
raise ConditionErrorMessage("time", f"unknown 'before' entity {before}")
- before = dt_util.dt.time(
- before_entity.attributes.get("hour", 23),
- before_entity.attributes.get("minute", 59),
- before_entity.attributes.get("second", 59),
- )
+ if before_entity.domain == "input_datetime":
+ before = dt_util.dt.time(
+ before_entity.attributes.get("hour", 23),
+ before_entity.attributes.get("minute", 59),
+ before_entity.attributes.get("second", 59),
+ )
+ elif before_entity.attributes.get(
+ ATTR_DEVICE_CLASS
+ ) == DEVICE_CLASS_TIMESTAMP and before_entity.state not in (
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+ ):
+ before_timedatime = dt_util.parse_datetime(before_entity.state)
+ if before_timedatime is None:
+ return False
+ before = dt_util.as_local(before_timedatime).time()
+ else:
+ return False
if after < before:
condition_trace_update_result(after=after, now_time=now_time, before=before)
@@ -902,6 +931,26 @@ async def async_device_from_config(
)
+async def async_trigger_from_config(
+ hass: HomeAssistant, config: ConfigType, config_validation: bool = True
+) -> ConditionCheckerType:
+ """Test a trigger condition."""
+ if config_validation:
+ config = cv.TRIGGER_CONDITION_SCHEMA(config)
+ trigger_id = config[CONF_ID]
+
+ @trace_condition_function
+ def trigger_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
+ """Validate trigger based if-condition."""
+ return (
+ variables is not None
+ and "trigger" in variables
+ and variables["trigger"].get("id") in trigger_id
+ )
+
+ return trigger_if
+
+
async def async_validate_condition_config(
hass: HomeAssistant, config: ConfigType | Template
) -> ConfigType | Template:
@@ -923,6 +972,8 @@ async def async_validate_condition_config(
platform = await async_get_device_automation_platform(
hass, config[CONF_DOMAIN], "condition"
)
+ if hasattr(platform, "async_validate_condition_config"):
+ return await platform.async_validate_condition_config(hass, config) # type: ignore
return cast(ConfigType, platform.CONDITION_SCHEMA(config)) # type: ignore
return config
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index ed619cc9678..e195c1ded31 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -45,6 +45,7 @@ from homeassistant.const import (
CONF_EVENT_DATA,
CONF_EVENT_DATA_TEMPLATE,
CONF_FOR,
+ CONF_ID,
CONF_PLATFORM,
CONF_REPEAT,
CONF_SCAN_INTERVAL,
@@ -770,25 +771,25 @@ def deprecated(
def key_value_schemas(
- key: str, value_schemas: dict[str, vol.Schema]
-) -> Callable[[Any], dict[str, Any]]:
+ key: str, value_schemas: dict[Hashable, vol.Schema]
+) -> Callable[[Any], dict[Hashable, Any]]:
"""Create a validator that validates based on a value for specific key.
This gives better error messages.
"""
- def key_value_validator(value: Any) -> dict[str, Any]:
+ def key_value_validator(value: Any) -> dict[Hashable, Any]:
if not isinstance(value, dict):
raise vol.Invalid("Expected a dictionary")
key_value = value.get(key)
- if key_value not in value_schemas:
- raise vol.Invalid(
- f"Unexpected value for {key}: '{key_value}'. Expected {', '.join(value_schemas)}"
- )
+ if isinstance(key_value, Hashable) and key_value in value_schemas:
+ return cast(Dict[Hashable, Any], value_schemas[key_value](value))
- return cast(Dict[str, Any], value_schemas[key_value](value))
+ raise vol.Invalid(
+ f"Unexpected value for {key}: '{key_value}'. Expected {', '.join(str(key) for key in value_schemas)}"
+ )
return key_value_validator
@@ -926,7 +927,7 @@ SERVICE_SCHEMA = vol.All(
)
NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any(
- vol.Coerce(float), vol.All(str, entity_domain("input_number"))
+ vol.Coerce(float), vol.All(str, entity_domain(["input_number", "number", "sensor"]))
)
CONDITION_BASE_SCHEMA = {vol.Optional(CONF_ALIAS): string}
@@ -1014,14 +1015,26 @@ TIME_CONDITION_SCHEMA = vol.All(
{
**CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "time",
- "before": vol.Any(time, vol.All(str, entity_domain("input_datetime"))),
- "after": vol.Any(time, vol.All(str, entity_domain("input_datetime"))),
+ "before": vol.Any(
+ time, vol.All(str, entity_domain(["input_datetime", "sensor"]))
+ ),
+ "after": vol.Any(
+ time, vol.All(str, entity_domain(["input_datetime", "sensor"]))
+ ),
"weekday": weekdays,
}
),
has_at_least_one_key("before", "after", "weekday"),
)
+TRIGGER_CONDITION_SCHEMA = vol.Schema(
+ {
+ **CONDITION_BASE_SCHEMA,
+ vol.Required(CONF_CONDITION): "trigger",
+ vol.Required(CONF_ID): vol.All(ensure_list, [string]),
+ }
+)
+
ZONE_CONDITION_SCHEMA = vol.Schema(
{
**CONDITION_BASE_SCHEMA,
@@ -1086,24 +1099,29 @@ CONDITION_SCHEMA: vol.Schema = vol.Schema(
key_value_schemas(
CONF_CONDITION,
{
+ "and": AND_CONDITION_SCHEMA,
+ "device": DEVICE_CONDITION_SCHEMA,
+ "not": NOT_CONDITION_SCHEMA,
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
+ "or": OR_CONDITION_SCHEMA,
"state": STATE_CONDITION_SCHEMA,
"sun": SUN_CONDITION_SCHEMA,
"template": TEMPLATE_CONDITION_SCHEMA,
"time": TIME_CONDITION_SCHEMA,
+ "trigger": TRIGGER_CONDITION_SCHEMA,
"zone": ZONE_CONDITION_SCHEMA,
- "and": AND_CONDITION_SCHEMA,
- "or": OR_CONDITION_SCHEMA,
- "not": NOT_CONDITION_SCHEMA,
- "device": DEVICE_CONDITION_SCHEMA,
},
),
dynamic_template,
)
)
+TRIGGER_BASE_SCHEMA = vol.Schema(
+ {vol.Required(CONF_PLATFORM): str, vol.Optional(CONF_ID): str}
+)
+
TRIGGER_SCHEMA = vol.All(
- ensure_list, [vol.Schema({vol.Required(CONF_PLATFORM): str}, extra=vol.ALLOW_EXTRA)]
+ ensure_list, [TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)]
)
_SCRIPT_DELAY_SCHEMA = vol.Schema(
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index 724280b19c9..187d53ea00b 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -10,7 +10,7 @@ import logging
import math
import sys
from timeit import default_timer as timer
-from typing import Any, TypedDict
+from typing import Any, TypedDict, final
from homeassistant.config import DATA_CUSTOMIZE
from homeassistant.const import (
@@ -93,6 +93,40 @@ def async_generate_entity_id(
return test_string
+def get_capability(hass: HomeAssistant, entity_id: str, capability: str) -> Any | None:
+ """Get a capability attribute of an entity.
+
+ First try the statemachine, then entity registry.
+ """
+ state = hass.states.get(entity_id)
+ if state:
+ return state.attributes.get(capability)
+
+ entity_registry = er.async_get(hass)
+ entry = entity_registry.async_get(entity_id)
+ if not entry:
+ raise HomeAssistantError(f"Unknown entity {entity_id}")
+
+ return entry.capabilities.get(capability) if entry.capabilities else None
+
+
+def get_device_class(hass: HomeAssistant, entity_id: str) -> str | None:
+ """Get device class of an entity.
+
+ First try the statemachine, then entity registry.
+ """
+ state = hass.states.get(entity_id)
+ if state:
+ return state.attributes.get(ATTR_DEVICE_CLASS)
+
+ entity_registry = er.async_get(hass)
+ entry = entity_registry.async_get(entity_id)
+ if not entry:
+ raise HomeAssistantError(f"Unknown entity {entity_id}")
+
+ return entry.device_class
+
+
def get_supported_features(hass: HomeAssistant, entity_id: str) -> int:
"""Get supported features for an entity.
@@ -110,6 +144,23 @@ def get_supported_features(hass: HomeAssistant, entity_id: str) -> int:
return entry.supported_features or 0
+def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None:
+ """Get unit of measurement class of an entity.
+
+ First try the statemachine, then entity registry.
+ """
+ state = hass.states.get(entity_id)
+ if state:
+ return state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+
+ entity_registry = er.async_get(hass)
+ entry = entity_registry.async_get(entity_id)
+ if not entry:
+ raise HomeAssistantError(f"Unknown entity {entity_id}")
+
+ return entry.unit_of_measurement
+
+
class DeviceInfo(TypedDict, total=False):
"""Entity device information for device registry."""
@@ -766,7 +817,11 @@ class Entity(ABC):
class ToggleEntity(Entity):
"""An abstract class for entities that can be turned on and off."""
+ _attr_is_on: bool
+ _attr_state: None = None
+
@property
+ @final
def state(self) -> str | None:
"""Return the state."""
return STATE_ON if self.is_on else STATE_OFF
@@ -774,7 +829,7 @@ class ToggleEntity(Entity):
@property
def is_on(self) -> bool:
"""Return True if entity is on."""
- raise NotImplementedError()
+ return self._attr_is_on
def turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
index b22fb9ec2d2..5436a01648e 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -508,7 +508,7 @@ class EntityPlatform:
entity.entity_id = entry.entity_id
if entry.disabled:
- self.logger.info(
+ self.logger.debug(
"Not adding entity %s because it's disabled",
entry.name
or entity.name
diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py
index 48dd05d2311..85eebf05298 100644
--- a/homeassistant/helpers/event.py
+++ b/homeassistant/helpers/event.py
@@ -1214,13 +1214,13 @@ track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_tim
@bind_hass
def async_call_later(
hass: HomeAssistant,
- delay: float,
+ delay: float | timedelta,
action: HassJob | Callable[..., Awaitable[None] | None],
) -> CALLBACK_TYPE:
"""Add a listener that is called in ."""
- return async_track_point_in_utc_time(
- hass, action, dt_util.utcnow() + timedelta(seconds=delay)
- )
+ if not isinstance(delay, timedelta):
+ delay = timedelta(seconds=delay)
+ return async_track_point_in_utc_time(hass, action, dt_util.utcnow() + delay)
call_later = threaded_listener_factory(async_call_later)
diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py
index ea3635888bb..156ceb8e612 100644
--- a/homeassistant/helpers/script.py
+++ b/homeassistant/helpers/script.py
@@ -256,7 +256,10 @@ async def async_validate_action_config(
platform = await device_automation.async_get_device_automation_platform(
hass, config[CONF_DOMAIN], "action"
)
- config = platform.ACTION_SCHEMA(config) # type: ignore
+ if hasattr(platform, "async_validate_action_config"):
+ config = await platform.async_validate_action_config(hass, config) # type: ignore
+ else:
+ config = platform.ACTION_SCHEMA(config) # type: ignore
elif action_type == cv.SCRIPT_ACTION_CHECK_CONDITION:
if config[CONF_CONDITION] == "device":
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index f65100a8775..d991a0b58f2 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -1420,6 +1420,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.filters["atan"] = arc_tangent
self.filters["atan2"] = arc_tangent2
self.filters["sqrt"] = square_root
+ self.filters["as_datetime"] = dt_util.parse_datetime
self.filters["as_timestamp"] = forgiving_as_timestamp
self.filters["as_local"] = dt_util.as_local
self.filters["timestamp_custom"] = timestamp_custom
@@ -1454,6 +1455,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["atan"] = arc_tangent
self.globals["atan2"] = arc_tangent2
self.globals["float"] = forgiving_float
+ self.globals["as_datetime"] = dt_util.parse_datetime
self.globals["as_local"] = dt_util.as_local
self.globals["as_timestamp"] = forgiving_as_timestamp
self.globals["relative_time"] = relative_time
diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py
index 045c56d964c..b5a82c3c020 100644
--- a/homeassistant/helpers/trigger.py
+++ b/homeassistant/helpers/trigger.py
@@ -8,7 +8,7 @@ from typing import Any, Callable
import voluptuous as vol
-from homeassistant.const import CONF_PLATFORM
+from homeassistant.const import CONF_ID, CONF_PLATFORM
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType
@@ -74,7 +74,10 @@ async def async_initialize_triggers(
triggers = []
for idx, conf in enumerate(trigger_config):
platform = await _async_get_trigger_platform(hass, conf)
- info = {**info, "trigger_id": f"{idx}"}
+ trigger_id = conf.get(CONF_ID, f"{idx}")
+ trigger_idx = f"{idx}"
+ trigger_data = {"id": trigger_id, "idx": trigger_idx}
+ info = {**info, "trigger_data": trigger_data}
triggers.append(platform.async_attach_trigger(hass, conf, action, info))
attach_results = await asyncio.gather(*triggers, return_exceptions=True)
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 29af3f52a07..908cd379886 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -4,7 +4,7 @@ aiodiscover==1.4.2
aiohttp==3.7.4.post0
aiohttp_cors==0.7.0
astral==2.2
-async-upnp-client==0.18.0
+async-upnp-client==0.19.0
async_timeout==3.0.1
attrs==21.2.0
awesomeversion==21.4.0
@@ -16,25 +16,24 @@ cryptography==3.3.2
defusedxml==0.7.1
distro==1.5.0
emoji==1.2.0
-hass-nabucasa==0.43.0
-home-assistant-frontend==20210603.0
+hass-nabucasa==0.44.0
+home-assistant-frontend==20210707.0
httpx==0.18.0
ifaddr==0.1.7
-jinja2>=3.0.1
-netdisco==2.8.3
+jinja2==3.0.1
paho-mqtt==1.5.1
-pillow==8.1.2
+pillow==8.2.0
pip>=8.0.3,<20.3
python-slugify==4.0.1
pyyaml==5.4.1
requests==2.25.1
ruamel.yaml==0.15.100
scapy==2.4.5
-sqlalchemy==1.4.13
+sqlalchemy==1.4.17
voluptuous-serialize==2.4.0
voluptuous==0.12.1
yarl==1.6.3
-zeroconf==0.31.0
+zeroconf==0.32.1
pycryptodome>=3.6.6
diff --git a/homeassistant/runner.py b/homeassistant/runner.py
index 86bebecb7b1..5eae0b1b2da 100644
--- a/homeassistant/runner.py
+++ b/homeassistant/runner.py
@@ -73,16 +73,6 @@ class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore[valid
loop.set_default_executor = warn_use( # type: ignore
loop.set_default_executor, "sets default executor on the event loop"
)
-
- # Shut down executor when we shut down loop
- orig_close = loop.close
-
- def close() -> None:
- executor.logged_shutdown()
- orig_close()
-
- loop.close = close # type: ignore
-
return loop
diff --git a/homeassistant/setup.py b/homeassistant/setup.py
index f5a6f9b9721..9b0c5282108 100644
--- a/homeassistant/setup.py
+++ b/homeassistant/setup.py
@@ -41,6 +41,7 @@ BASE_PLATFORMS = {
"notify",
"remote",
"scene",
+ "select",
"sensor",
"switch",
"tts",
diff --git a/homeassistant/util/decorator.py b/homeassistant/util/decorator.py
index 83c63711c7d..d2943d39979 100644
--- a/homeassistant/util/decorator.py
+++ b/homeassistant/util/decorator.py
@@ -1,4 +1,5 @@
"""Decorator utility functions."""
+from collections.abc import Hashable
from typing import Callable, TypeVar
CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name
@@ -7,7 +8,7 @@ CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-na
class Registry(dict):
"""Registry of items."""
- def register(self, name: str) -> Callable[[CALLABLE_T], CALLABLE_T]:
+ def register(self, name: Hashable) -> Callable[[CALLABLE_T], CALLABLE_T]:
"""Return decorator to register item with a specific name."""
def decorator(func: CALLABLE_T) -> CALLABLE_T:
diff --git a/homeassistant/util/executor.py b/homeassistant/util/executor.py
index c25c6b9c13f..9277e396bc4 100644
--- a/homeassistant/util/executor.py
+++ b/homeassistant/util/executor.py
@@ -62,7 +62,7 @@ def join_or_interrupt_threads(
class InterruptibleThreadPoolExecutor(ThreadPoolExecutor):
"""A ThreadPoolExecutor instance that will not deadlock on shutdown."""
- def logged_shutdown(self) -> None:
+ def shutdown(self, *args, **kwargs) -> None: # type: ignore
"""Shutdown backport from cpython 3.9 with interrupt support added."""
with self._shutdown_lock: # type: ignore[attr-defined]
self._shutdown = True
diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py
index 22ad86a6896..24ad3242921 100644
--- a/homeassistant/util/pressure.py
+++ b/homeassistant/util/pressure.py
@@ -32,7 +32,7 @@ def convert(value: float, unit_1: str, unit_2: str) -> float:
if not isinstance(value, Number):
raise TypeError(f"{value} is not of numeric type")
- if unit_1 == unit_2 or unit_1 not in VALID_UNITS:
+ if unit_1 == unit_2:
return value
pascals = value / UNIT_CONVERSION[unit_1]
diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py
index 0b3edc6ef57..bc3cb4c1017 100644
--- a/homeassistant/util/temperature.py
+++ b/homeassistant/util/temperature.py
@@ -2,6 +2,7 @@
from homeassistant.const import (
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
+ TEMP_KELVIN,
TEMPERATURE,
UNIT_NOT_RECOGNIZED_TEMPLATE,
)
@@ -14,6 +15,13 @@ def fahrenheit_to_celsius(fahrenheit: float, interval: bool = False) -> float:
return (fahrenheit - 32.0) / 1.8
+def kelvin_to_celsius(kelvin: float, interval: bool = False) -> float:
+ """Convert a temperature in Kelvin to Celsius."""
+ if interval:
+ return kelvin
+ return kelvin - 273.15
+
+
def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float:
"""Convert a temperature in Celsius to Fahrenheit."""
if interval:
@@ -21,17 +29,39 @@ def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float:
return celsius * 1.8 + 32.0
+def celsius_to_kelvin(celsius: float, interval: bool = False) -> float:
+ """Convert a temperature in Celsius to Fahrenheit."""
+ if interval:
+ return celsius
+ return celsius + 273.15
+
+
def convert(
temperature: float, from_unit: str, to_unit: str, interval: bool = False
) -> float:
"""Convert a temperature from one unit to another."""
- if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
+ if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN):
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, TEMPERATURE))
- if to_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
+ if to_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN):
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, TEMPERATURE))
if from_unit == to_unit:
return temperature
+
if from_unit == TEMP_CELSIUS:
- return celsius_to_fahrenheit(temperature, interval)
- return fahrenheit_to_celsius(temperature, interval)
+ if to_unit == TEMP_FAHRENHEIT:
+ return celsius_to_fahrenheit(temperature, interval)
+ # kelvin
+ return celsius_to_kelvin(temperature, interval)
+
+ if from_unit == TEMP_FAHRENHEIT:
+ if to_unit == TEMP_CELSIUS:
+ return fahrenheit_to_celsius(temperature, interval)
+ # kelvin
+ return celsius_to_kelvin(fahrenheit_to_celsius(temperature, interval), interval)
+
+ # from_unit == kelvin
+ if to_unit == TEMP_CELSIUS:
+ return kelvin_to_celsius(temperature, interval)
+ # fahrenheit
+ return celsius_to_fahrenheit(kelvin_to_celsius(temperature, interval), interval)
diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py
index 5e98e4cfc6f..58edac6d280 100644
--- a/homeassistant/util/yaml/loader.py
+++ b/homeassistant/util/yaml/loader.py
@@ -98,10 +98,10 @@ class SafeLineLoader(yaml.SafeLoader):
super().__init__(stream)
self.secrets = secrets
- def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node:
+ def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node: # type: ignore[override]
"""Annotate a node with the first line it was seen."""
last_line: int = self.line
- node: yaml.nodes.Node = super().compose_node(parent, index)
+ node: yaml.nodes.Node = super().compose_node(parent, index) # type: ignore[assignment]
node.__line__ = last_line + 1 # type: ignore
return node
@@ -264,7 +264,7 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order
fname = getattr(loader.stream, "name", "")
raise yaml.MarkedYAMLError(
context=f'invalid key: "{key}"',
- context_mark=yaml.Mark(fname, 0, line, -1, None, None),
+ context_mark=yaml.Mark(fname, 0, line, -1, None, None), # type: ignore[arg-type]
) from exc
if key in seen:
diff --git a/machine/generic-x86-64 b/machine/generic-x86-64
index 4c83228387d..b7fbac2e8ed 100644
--- a/machine/generic-x86-64
+++ b/machine/generic-x86-64
@@ -4,31 +4,3 @@ FROM homeassistant/amd64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
libva-intel-driver \
usbutils
-
-##
-# Build libcec for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- p8-platform-dev \
- linux-headers \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && cd /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- -DHAVE_LINUX_API=1 \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec*
diff --git a/machine/intel-nuc b/machine/intel-nuc
index b5538b8ccad..5e1b7f957d1 100644
--- a/machine/intel-nuc
+++ b/machine/intel-nuc
@@ -7,31 +7,3 @@ FROM homeassistant/amd64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
libva-intel-driver \
usbutils
-
-##
-# Build libcec for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- p8-platform-dev \
- linux-headers \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && cd /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- -DHAVE_LINUX_API=1 \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec*
diff --git a/machine/odroid-c2 b/machine/odroid-c2
index 9bfbb931ed0..be07d6c8aba 100644
--- a/machine/odroid-c2
+++ b/machine/odroid-c2
@@ -3,32 +3,3 @@ FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
-
-##
-# Build libcec for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- p8-platform-dev \
- linux-headers \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && cd /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- -DHAVE_LINUX_API=1 \
- -DHAVE_AOCEC_API=1 \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec*
diff --git a/machine/odroid-c4 b/machine/odroid-c4
index 9bfbb931ed0..be07d6c8aba 100644
--- a/machine/odroid-c4
+++ b/machine/odroid-c4
@@ -3,32 +3,3 @@ FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
-
-##
-# Build libcec for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- p8-platform-dev \
- linux-headers \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && cd /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- -DHAVE_LINUX_API=1 \
- -DHAVE_AOCEC_API=1 \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec*
diff --git a/machine/odroid-n2 b/machine/odroid-n2
index 9bfbb931ed0..be07d6c8aba 100644
--- a/machine/odroid-n2
+++ b/machine/odroid-n2
@@ -3,32 +3,3 @@ FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
-
-##
-# Build libcec for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- p8-platform-dev \
- linux-headers \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && cd /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- -DHAVE_LINUX_API=1 \
- -DHAVE_AOCEC_API=1 \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec*
diff --git a/machine/odroid-xu b/machine/odroid-xu
index 1947115f672..3aa428d3f52 100644
--- a/machine/odroid-xu
+++ b/machine/odroid-xu
@@ -3,32 +3,3 @@ FROM homeassistant/armv7-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
-
-##
-# Build libcec for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- p8-platform-dev \
- linux-headers \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && cd /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- -DHAVE_LINUX_API=1 \
- -DHAVE_EXYNOS_API=1 \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec*
diff --git a/machine/qemuarm b/machine/qemuarm
index 2735a7bae23..e00c945e945 100644
--- a/machine/qemuarm
+++ b/machine/qemuarm
@@ -3,31 +3,3 @@ FROM homeassistant/armhf-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
-
-##
-# Build libcec for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- p8-platform-dev \
- linux-headers \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && cd /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- -DHAVE_LINUX_API=1 \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec*
diff --git a/machine/qemuarm-64 b/machine/qemuarm-64
index 5783de82f58..be07d6c8aba 100644
--- a/machine/qemuarm-64
+++ b/machine/qemuarm-64
@@ -3,31 +3,3 @@ FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
-
-##
-# Build libcec for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- p8-platform-dev \
- linux-headers \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && cd /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- -DHAVE_LINUX_API=1 \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec*
diff --git a/machine/qemux86 b/machine/qemux86
index 192d287dfde..1b5350df4c8 100644
--- a/machine/qemux86
+++ b/machine/qemux86
@@ -3,31 +3,3 @@ FROM homeassistant/i386-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
-
-##
-# Build libcec for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- p8-platform-dev \
- linux-headers \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && cd /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- -DHAVE_LINUX_API=1 \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec*
diff --git a/machine/qemux86-64 b/machine/qemux86-64
index 5f4ca461ae8..541e994b967 100644
--- a/machine/qemux86-64
+++ b/machine/qemux86-64
@@ -3,31 +3,3 @@ FROM homeassistant/amd64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
-
-##
-# Build libcec for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- p8-platform-dev \
- linux-headers \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && cd /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- -DHAVE_LINUX_API=1 \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec*
diff --git a/machine/raspberrypi b/machine/raspberrypi
index c9271aceccb..3f000b14db7 100644
--- a/machine/raspberrypi
+++ b/machine/raspberrypi
@@ -15,32 +15,3 @@ RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \
&& ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \
&& ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \
&& ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv
-
-##
-# Build libcec with RPi support for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- raspberrypi-dev \
- p8-platform-dev \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DRPI_INCLUDE_DIR=/opt/vc/include \
- -DRPI_LIB_DIR=/opt/vc/lib \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec
-ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH}
diff --git a/machine/raspberrypi2 b/machine/raspberrypi2
index d6c01b4ae02..484b209b6fa 100644
--- a/machine/raspberrypi2
+++ b/machine/raspberrypi2
@@ -15,32 +15,3 @@ RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \
&& ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \
&& ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \
&& ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv
-
-##
-# Build libcec with RPi support for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- raspberrypi-dev \
- p8-platform-dev \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DRPI_INCLUDE_DIR=/opt/vc/include \
- -DRPI_LIB_DIR=/opt/vc/lib \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec
-ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH}
diff --git a/machine/raspberrypi3 b/machine/raspberrypi3
index 4509e150584..1aec7ebf39f 100644
--- a/machine/raspberrypi3
+++ b/machine/raspberrypi3
@@ -15,32 +15,3 @@ RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \
&& ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \
&& ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \
&& ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv
-
-##
-# Build libcec with RPi support for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- raspberrypi-dev \
- p8-platform-dev \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DRPI_INCLUDE_DIR=/opt/vc/include \
- -DRPI_LIB_DIR=/opt/vc/lib \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec
-ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH}
diff --git a/machine/raspberrypi3-64 b/machine/raspberrypi3-64
index 97064a2377d..165dc2e5397 100644
--- a/machine/raspberrypi3-64
+++ b/machine/raspberrypi3-64
@@ -15,32 +15,3 @@ RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \
&& ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \
&& ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \
&& ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv
-
-##
-# Build libcec with RPi support for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- raspberrypi-dev \
- p8-platform-dev \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DRPI_INCLUDE_DIR=/opt/vc/include \
- -DRPI_LIB_DIR=/opt/vc/lib \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec
-ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH}
diff --git a/machine/raspberrypi4 b/machine/raspberrypi4
index 4509e150584..1aec7ebf39f 100644
--- a/machine/raspberrypi4
+++ b/machine/raspberrypi4
@@ -15,32 +15,3 @@ RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \
&& ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \
&& ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \
&& ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv
-
-##
-# Build libcec with RPi support for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- raspberrypi-dev \
- p8-platform-dev \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DRPI_INCLUDE_DIR=/opt/vc/include \
- -DRPI_LIB_DIR=/opt/vc/lib \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec
-ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH}
diff --git a/machine/raspberrypi4-64 b/machine/raspberrypi4-64
index 97064a2377d..165dc2e5397 100644
--- a/machine/raspberrypi4-64
+++ b/machine/raspberrypi4-64
@@ -15,32 +15,3 @@ RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \
&& ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \
&& ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \
&& ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv
-
-##
-# Build libcec with RPi support for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- raspberrypi-dev \
- p8-platform-dev \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DRPI_INCLUDE_DIR=/opt/vc/include \
- -DRPI_LIB_DIR=/opt/vc/lib \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec
-ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH}
diff --git a/machine/tinker b/machine/tinker
index 46b627c2257..04a0aa6dc2c 100644
--- a/machine/tinker
+++ b/machine/tinker
@@ -7,42 +7,3 @@ RUN apk --no-cache add usbutils \
bluepy \
pybluez \
pygatt[GATTTOOL]
-
-# Install GPIO support
-RUN apk add --no-cache --virtual .build-dependencies \
- gcc libc-dev musl-dev \
- && git clone --depth 1 https://github.com/TinkerBoard/gpio_lib_python /usr/src/gpio \
- && cd /usr/src/gpio \
- && sed -i "s/caddr_t/void*/g" source/wiringTB.c \
- && export MAKEFLAGS="-j$(nproc)" \
- && python3 setup.py install \
- && apk del .build-dependencies \
- && rm -rf /usr/src/gpio
-
-##
-# Build libcec for HDMI-CEC
-ARG LIBCEC_VERSION=6.0.2
-RUN apk add --no-cache \
- eudev-libs \
- p8-platform \
- && apk add --no-cache --virtual .build-dependencies \
- build-base \
- cmake \
- eudev-dev \
- swig \
- p8-platform-dev \
- linux-headers \
- && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
- && cd /usr/src/libcec \
- && mkdir -p /usr/src/libcec/build \
- && cd /usr/src/libcec/build \
- && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
- -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
- -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
- -DHAVE_LINUX_API=1 \
- .. \
- && make -j$(nproc) \
- && make install \
- && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
- && apk del .build-dependencies \
- && rm -rf /usr/src/libcec*
diff --git a/mypy.ini b/mypy.ini
index c65f28336ff..4472311279f 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -143,6 +143,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.ambee.*]
+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.ampio.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -275,6 +286,28 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.dnsip.*]
+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.dsmr.*]
+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.dunehd.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -308,6 +341,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.forecast_solar.*]
+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.fritzbox.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -374,6 +418,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.homeassistant.triggers.event]
+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.http.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -462,6 +517,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.local_ip.*]
+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.lock.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -495,6 +561,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.mysensors.*]
+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.nam.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -517,6 +594,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.no_ip.*]
+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.notify.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -561,6 +649,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.pi_hole.*]
+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.proximity.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -627,6 +726,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.select.*]
+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.sensor.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -660,6 +770,28 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.ssdp.*]
+check_untyped_defs = true
+disallow_incomplete_defs = true
+disallow_subclassing_any = true
+disallow_untyped_calls = true
+disallow_untyped_decorators = true
+disallow_untyped_defs = true
+no_implicit_optional = true
+warn_return_any = true
+warn_unreachable = true
+
+[mypy-homeassistant.components.stream.*]
+check_untyped_defs = true
+disallow_incomplete_defs = true
+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.sun.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -737,6 +869,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.uptime.*]
+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
@@ -781,6 +924,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.zodiac.*]
+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.zeroconf.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -921,9 +1075,6 @@ ignore_errors = true
[mypy-homeassistant.components.doorbird.*]
ignore_errors = true
-[mypy-homeassistant.components.dsmr.*]
-ignore_errors = true
-
[mypy-homeassistant.components.dynalite.*]
ignore_errors = true
@@ -1038,7 +1189,22 @@ ignore_errors = true
[mypy-homeassistant.components.home_plus_control.*]
ignore_errors = true
-[mypy-homeassistant.components.homeassistant.*]
+[mypy-homeassistant.components.homeassistant.triggers.homeassistant]
+ignore_errors = true
+
+[mypy-homeassistant.components.homeassistant.triggers.numeric_state]
+ignore_errors = true
+
+[mypy-homeassistant.components.homeassistant.triggers.time_pattern]
+ignore_errors = true
+
+[mypy-homeassistant.components.homeassistant.triggers.time]
+ignore_errors = true
+
+[mypy-homeassistant.components.homeassistant.triggers.state]
+ignore_errors = true
+
+[mypy-homeassistant.components.homeassistant.scene]
ignore_errors = true
[mypy-homeassistant.components.homekit.*]
@@ -1074,9 +1240,6 @@ ignore_errors = true
[mypy-homeassistant.components.influxdb.*]
ignore_errors = true
-[mypy-homeassistant.components.input_boolean.*]
-ignore_errors = true
-
[mypy-homeassistant.components.input_datetime.*]
ignore_errors = true
@@ -1164,9 +1327,6 @@ ignore_errors = true
[mypy-homeassistant.components.mullvad.*]
ignore_errors = true
-[mypy-homeassistant.components.mysensors.*]
-ignore_errors = true
-
[mypy-homeassistant.components.neato.*]
ignore_errors = true
diff --git a/requirements.txt b/requirements.txt
index 7d9b7739669..ad9c2717e94 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,7 +11,7 @@ bcrypt==3.1.7
certifi>=2020.12.5
ciso8601==2.1.3
httpx==0.18.0
-jinja2>=3.0.1
+jinja2==3.0.1
PyJWT==1.7.1
cryptography==3.3.2
pip>=8.0.3,<20.3
diff --git a/requirements_all.txt b/requirements_all.txt
index 65a981fa039..5efcc2d3d3a 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2
# Adafruit_BBIO==1.1.1
# homeassistant.components.homekit
-HAP-python==3.5.0
+HAP-python==3.5.1
# homeassistant.components.mastodon
Mastodon.py==1.5.1
@@ -105,7 +105,7 @@ adafruit-circuitpython-dht==3.6.0
adafruit-circuitpython-mcp230xx==2.2.2
# homeassistant.components.androidtv
-adb-shell[async]==0.3.1
+adb-shell[async]==0.3.4
# homeassistant.components.alarmdecoder
adext==0.4.2
@@ -123,16 +123,16 @@ afsapi==0.0.4
agent-py==0.0.23
# homeassistant.components.geonetnz_quakes
-aio_geojson_geonetnz_quakes==0.12
+aio_geojson_geonetnz_quakes==0.13
# homeassistant.components.geonetnz_volcano
-aio_geojson_geonetnz_volcano==0.5
+aio_geojson_geonetnz_volcano==0.6
# homeassistant.components.nsw_rural_fire_service_feed
-aio_geojson_nsw_rfs_incidents==0.3
+aio_geojson_nsw_rfs_incidents==0.4
# homeassistant.components.gdacs
-aio_georss_gdacs==0.4
+aio_georss_gdacs==0.5
# homeassistant.components.ambient_station
aioambient==1.2.4
@@ -160,7 +160,7 @@ aioeafm==0.1.2
aioemonitor==1.0.5
# homeassistant.components.esphome
-aioesphomeapi==2.8.0
+aioesphomeapi==4.0.1
# homeassistant.components.flo
aioflo==0.4.1
@@ -175,17 +175,17 @@ aioguardian==1.0.4
aioharmony==0.2.7
# homeassistant.components.homekit_controller
-aiohomekit==0.2.67
+aiohomekit==0.4.2
# homeassistant.components.emulated_hue
# homeassistant.components.http
aiohttp_cors==0.7.0
# homeassistant.components.hue
-aiohue==2.5.0
+aiohue==2.5.1
# homeassistant.components.imap
-aioimaplib==0.7.15
+aioimaplib==0.9.0
# homeassistant.components.apache_kafka
aiokafka==0.6.0
@@ -205,6 +205,12 @@ aiolip==1.1.4
# homeassistant.components.lyric
aiolyric==1.0.7
+# homeassistant.components.modern_forms
+aiomodernforms==0.1.8
+
+# homeassistant.components.yamaha_musiccast
+aiomusiccast==0.8.0
+
# homeassistant.components.keyboard_remote
aionotify==0.2.0
@@ -218,7 +224,7 @@ aiopulse==0.4.2
aiopvapi==1.6.14
# homeassistant.components.pvpc_hourly_pricing
-aiopvpc==2.1.2
+aiopvpc==2.2.0
# homeassistant.components.webostv
aiopylgtv==0.4.0
@@ -230,7 +236,7 @@ aiorecollect==1.0.5
aioshelly==0.6.4
# homeassistant.components.switcher_kis
-aioswitcher==1.2.1
+aioswitcher==1.2.3
# homeassistant.components.syncthing
aiosyncthing==0.5.1
@@ -250,6 +256,9 @@ aladdin_connect==0.3
# homeassistant.components.alpha_vantage
alpha_vantage==2.3.1
+# homeassistant.components.ambee
+ambee==0.3.0
+
# homeassistant.components.ambiclimate
ambiclimate==0.2.1
@@ -257,7 +266,7 @@ ambiclimate==0.2.1
amcrest==1.7.2
# homeassistant.components.androidtv
-androidtv[async]==0.0.59
+androidtv[async]==0.0.60
# homeassistant.components.anel_pwrctrl
anel_pwrctrl-homeassistant==0.0.1.dev2
@@ -295,7 +304,7 @@ asterisk_mbox==0.5.0
# homeassistant.components.dlna_dmr
# homeassistant.components.ssdp
# homeassistant.components.upnp
-async-upnp-client==0.18.0
+async-upnp-client==0.19.0
# homeassistant.components.supla
asyncpysupla==0.0.5
@@ -322,7 +331,7 @@ av==8.0.3
axis==44
# homeassistant.components.azure_event_hub
-azure-eventhub==5.1.0
+azure-eventhub==5.5.0
# homeassistant.components.azure_service_bus
azure-servicebus==0.50.3
@@ -349,7 +358,7 @@ beautifulsoup4==4.9.3
# beewi_smartclim==0.0.10
# homeassistant.components.zha
-bellows==0.24.0
+bellows==0.25.0
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.7.15
@@ -383,7 +392,7 @@ blockchain==1.4.4
bond-api==0.1.12
# homeassistant.components.bosch_shc
-boschshcpy==0.2.17
+boschshcpy==0.2.19
# homeassistant.components.amazon_polly
# homeassistant.components.route53
@@ -408,7 +417,7 @@ brunt==0.1.3
bsblan==0.4.0
# homeassistant.components.bluetooth_tracker
-bt_proximity==0.2
+bt_proximity==0.2.1
# homeassistant.components.bt_home_hub_5
bthomehub5-devicelist==0.1.1
@@ -464,7 +473,7 @@ coronavirus==1.1.1
datadog==0.15.0
# homeassistant.components.metoffice
-datapoint==0.9.5
+datapoint==0.9.8
# homeassistant.components.debugpy
debugpy==1.3.0
@@ -551,7 +560,7 @@ emulated_roku==0.2.1
enocean==0.50
# homeassistant.components.entur_public_transport
-enturclient==0.2.1
+enturclient==0.2.2
# homeassistant.components.environment_canada
env_canada==0.2.5
@@ -614,8 +623,11 @@ fnvhash==0.1.0
# homeassistant.components.foobot
foobot_async==1.0.0
+# homeassistant.components.forecast_solar
+forecast_solar==1.3.1
+
# homeassistant.components.fortios
-fortiosapi==0.10.8
+fortiosapi==1.0.5
# homeassistant.components.freebox
freebox-api==0.0.10
@@ -628,8 +640,11 @@ freesms==0.2.0
# homeassistant.components.fritzbox_netmonitor
fritzconnection==1.4.2
+# homeassistant.components.fritz
+fritzprofiles==0.6.1
+
# homeassistant.components.google_translate
-gTTS==2.2.2
+gTTS==2.2.3
# homeassistant.components.garages_amsterdam
garages-amsterdam==2.1.1
@@ -648,13 +663,13 @@ geojson_client==0.6
geopy==2.1.0
# homeassistant.components.geo_rss_events
-georss_generic_client==0.4
+georss_generic_client==0.6
# homeassistant.components.ign_sismologia
-georss_ign_sismologia_client==0.2
+georss_ign_sismologia_client==0.3
# homeassistant.components.qld_bushfire
-georss_qld_bushfire_alert_client==0.3
+georss_qld_bushfire_alert_client==0.5
# homeassistant.components.huawei_lte
# homeassistant.components.kef
@@ -663,7 +678,7 @@ georss_qld_bushfire_alert_client==0.3
getmac==0.8.2
# homeassistant.components.gios
-gios==1.0.1
+gios==1.0.2
# homeassistant.components.gitter
gitterpy==0.1.7
@@ -702,7 +717,7 @@ gpiozero==1.5.1
gps3==0.33.3
# homeassistant.components.gree
-greeclimate==0.11.4
+greeclimate==0.11.7
# homeassistant.components.greeneye_monitor
greeneye_monitor==2.1
@@ -723,22 +738,22 @@ guppy3==3.1.0
ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
-ha-philipsjs==2.7.3
+ha-philipsjs==2.7.4
# homeassistant.components.habitica
habitipy==0.2.0
# homeassistant.components.hangouts
-hangups==0.4.11
+hangups==0.4.14
# homeassistant.components.cloud
-hass-nabucasa==0.43.0
+hass-nabucasa==0.44.0
# homeassistant.components.splunk
hass_splunk==0.1.1
# homeassistant.components.tasmota
-hatasmota==0.2.14
+hatasmota==0.2.19
# homeassistant.components.jewish_calendar
hdate==0.10.2
@@ -765,7 +780,7 @@ hole==0.5.1
holidays==0.11.1
# homeassistant.components.frontend
-home-assistant-frontend==20210603.0
+home-assistant-frontend==20210707.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -816,7 +831,7 @@ ibm-watson==5.1.0
ibmiotf==0.3.4
# homeassistant.components.ping
-icmplib==2.1.1
+icmplib==3.0
# homeassistant.components.network
ifaddr==0.1.7
@@ -933,13 +948,13 @@ maxcube-api==0.4.3
mbddns==0.1.2
# homeassistant.components.minecraft_server
-mcstatus==5.1.1
+mcstatus==6.0.0
# homeassistant.components.message_bird
messagebird==1.2.0
# homeassistant.components.meteoalarm
-meteoalertapi==0.1.6
+meteoalertapi==0.2.0
# homeassistant.components.meteo_france
meteofrance-api==1.0.2
@@ -947,11 +962,14 @@ meteofrance-api==1.0.2
# homeassistant.components.mfi
mficlient==0.3.0
+# homeassistant.components.xiaomi_miio
+micloud==0.3
+
# homeassistant.components.miflora
miflora==0.7.0
# homeassistant.components.mill
-millheater==0.4.1
+millheater==0.5.0
# homeassistant.components.minio
minio==4.0.9
@@ -981,7 +999,7 @@ mychevy==2.1.1
mycroftapi==2.0
# homeassistant.components.nad
-nad_receiver==0.0.12
+nad_receiver==0.2.0
# homeassistant.components.keenetic_ndms2
ndms2_client==0.1.1
@@ -993,11 +1011,10 @@ nessclient==0.9.15
netdata==0.2.0
# homeassistant.components.discovery
-# homeassistant.components.ssdp
-netdisco==2.8.3
+netdisco==2.9.0
# homeassistant.components.nam
-nettigo-air-monitor==0.2.6
+nettigo-air-monitor==1.0.0
# homeassistant.components.neurio_energy
neurio==0.3.1
@@ -1024,7 +1041,7 @@ notify-events==1.0.4
nsapi==3.0.4
# homeassistant.components.nsw_fuel_station
-nsw-fuel-api-client==1.0.10
+nsw-fuel-api-client==1.1.0
# homeassistant.components.nuheat
nuheat==0.3.0
@@ -1149,13 +1166,13 @@ pilight==0.1.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
-pillow==8.1.2
+pillow==8.2.0
# homeassistant.components.dominos
pizzapi==0.0.3
# homeassistant.components.plex
-plexapi==4.5.1
+plexapi==4.6.1
# homeassistant.components.plex
plexauth==0.0.6
@@ -1180,7 +1197,7 @@ poolsense==0.0.8
praw==7.2.0
# homeassistant.components.islamic_prayer_times
-prayer_times_calculator==0.0.3
+prayer_times_calculator==0.0.5
# homeassistant.components.progettihwsw
progettihwsw==0.1.1
@@ -1259,7 +1276,7 @@ pyRFXtrx==0.27.0
# pySwitchmate==0.4.6
# homeassistant.components.tibber
-pyTibber==0.17.0
+pyTibber==0.18.0
# homeassistant.components.dlink
pyW215==0.7.0
@@ -1295,7 +1312,7 @@ pyarlo==0.2.4
pyatag==0.3.5.3
# homeassistant.components.netatmo
-pyatmo==5.0.1
+pyatmo==5.2.0
# homeassistant.components.atome
pyatome==0.1.1
@@ -1313,7 +1330,7 @@ pyblackbird==0.5
# pybluez==0.22
# homeassistant.components.neato
-pybotvac==0.0.20
+pybotvac==0.0.21
# homeassistant.components.nissan_leaf
pycarwings2==2.10
@@ -1325,7 +1342,7 @@ pycfdns==1.2.1
pychannels==1.0.0
# homeassistant.components.cast
-pychromecast==9.1.2
+pychromecast==9.2.0
# homeassistant.components.pocketcasts
pycketcasts==1.0.0
@@ -1352,13 +1369,13 @@ pycsspeechtts==1.0.4
# pycups==1.9.73
# homeassistant.components.daikin
-pydaikin==2.4.3
+pydaikin==2.4.4
# homeassistant.components.danfoss_air
pydanfossair==0.1.0
# homeassistant.components.deconz
-pydeconz==79
+pydeconz==80
# homeassistant.components.delijn
pydelijn==0.6.1
@@ -1385,7 +1402,7 @@ pyeconet==0.1.14
pyedimax==0.2.1
# homeassistant.components.eight_sleep
-pyeight==0.1.5
+pyeight==0.1.9
# homeassistant.components.emby
pyemby==1.7
@@ -1400,7 +1417,7 @@ pyephember==0.3.1
pyeverlights==0.1.0
# homeassistant.components.ezviz
-pyezviz==0.1.8.7
+pyezviz==0.1.8.9
# homeassistant.components.fido
pyfido==2.1.1
@@ -1426,11 +1443,14 @@ pyfnip==0.2
# homeassistant.components.forked_daapd
pyforked-daapd==0.1.11
+# homeassistant.components.freedompro
+pyfreedompro==1.1.0
+
# homeassistant.components.fritzbox
pyfritzhome==0.4.2
# homeassistant.components.fronius
-pyfronius==0.4.6
+pyfronius==0.5.2
# homeassistant.components.ifttt
pyfttt==0.3
@@ -1440,7 +1460,7 @@ pyfttt==0.3
pygatt[GATTTOOL]==4.0.5
# homeassistant.components.gtfs
-pygtfs==0.1.5
+pygtfs==0.1.6
# homeassistant.components.hvv_departures
pygti==0.9.2
@@ -1458,7 +1478,7 @@ pyhik==0.2.8
pyhiveapi==0.4.2
# homeassistant.components.homematic
-pyhomematic==0.1.72
+pyhomematic==0.1.73
# homeassistant.components.homeworks
pyhomeworks==0.0.6
@@ -1563,7 +1583,7 @@ pymelcloud==2.5.3
pymeteoclimatic==0.0.6
# homeassistant.components.somfy
-pymfy==0.9.3
+pymfy==0.11.0
# homeassistant.components.xiaomi_tv
pymitv==1.4.3
@@ -1580,9 +1600,6 @@ pymonoprice==0.3
# homeassistant.components.msteams
pymsteams==0.1.12
-# homeassistant.components.yamaha_musiccast
-pymusiccast==0.1.6
-
# homeassistant.components.myq
pymyq==3.0.4
@@ -1691,7 +1708,7 @@ pyrepetier==3.0.5
pyrisco==0.3.1
# homeassistant.components.rituals_perfume_genie
-pyrituals==0.0.3
+pyrituals==0.0.4
# homeassistant.components.ruckus_unleashed
pyruckus==0.12
@@ -1732,7 +1749,7 @@ pysignalclirestapi==0.3.4
pyskyqhub==0.1.3
# homeassistant.components.sma
-pysma==0.4.3
+pysma==0.6.2
# homeassistant.components.smappee
pysmappee==0.2.25
@@ -1756,7 +1773,7 @@ pysnmp==4.4.12
pysoma==0.0.10
# homeassistant.components.sonos
-pysonos==0.0.49
+pysonos==0.0.51
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -1822,7 +1839,7 @@ python-gitlab==1.6.0
python-hpilo==4.3
# homeassistant.components.izone
-python-izone==1.1.4
+python-izone==1.1.6
# homeassistant.components.joaoapps_join
python-join-api==0.0.6
@@ -1903,7 +1920,7 @@ python_opendata_transport==0.2.1
pythonegardia==1.0.40
# homeassistant.components.tile
-pytile==5.2.0
+pytile==5.2.2
# homeassistant.components.touchline
pytouchline==0.7
@@ -2085,7 +2102,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
-simplisafe-python==10.0.0
+simplisafe-python==11.0.0
# homeassistant.components.sisyphus
sisyphus-control==3.0
@@ -2154,7 +2171,7 @@ spotipy==2.18.0
# homeassistant.components.recorder
# homeassistant.components.sql
-sqlalchemy==1.4.13
+sqlalchemy==1.4.17
# homeassistant.components.srp_energy
srpenergy==1.3.2
@@ -2339,7 +2356,7 @@ wallbox==0.4.4
waqiasync==1.0.0
# homeassistant.components.folder_watcher
-watchdog==2.1.2
+watchdog==2.1.3
# homeassistant.components.waterfurnace
waterfurnace==1.1.0
@@ -2347,9 +2364,6 @@ waterfurnace==1.1.0
# homeassistant.components.cisco_webex_teams
webexteamssdk==1.1.1
-# homeassistant.components.gpmdp
-websocket-client==0.54.0
-
# homeassistant.components.wiffi
wiffi==1.0.1
@@ -2360,10 +2374,10 @@ wirelesstagpy==0.4.1
withings-api==2.3.2
# homeassistant.components.wled
-wled==0.4.4
+wled==0.7.1
# homeassistant.components.wolflink
-wolf_smartset==0.1.8
+wolf_smartset==0.1.11
# homeassistant.components.xbee
xbee-helper==0.0.7
@@ -2375,9 +2389,10 @@ xbox-webapi==2.0.11
xboxapi==2.0.1
# homeassistant.components.knx
-xknx==0.18.4
+xknx==0.18.8
# homeassistant.components.bluesound
+# homeassistant.components.fritz
# homeassistant.components.rest
# homeassistant.components.startca
# homeassistant.components.ted5000
@@ -2409,10 +2424,10 @@ zeep[async]==4.0.0
zengge==0.2
# homeassistant.components.zeroconf
-zeroconf==0.31.0
+zeroconf==0.32.1
# homeassistant.components.zha
-zha-quirks==0.0.57
+zha-quirks==0.0.59
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9
@@ -2436,10 +2451,10 @@ zigpy-zigate==0.7.3
zigpy-znp==0.5.1
# homeassistant.components.zha
-zigpy==0.33.0
+zigpy==0.35.1
# homeassistant.components.zoneminder
zm-py==0.5.2
# homeassistant.components.zwave_js
-zwave-js-server-python==0.26.1
+zwave-js-server-python==0.27.0
diff --git a/requirements_test.txt b/requirements_test.txt
index 02b041d6074..660ea2a11fd 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -4,17 +4,17 @@
-c homeassistant/package_constraints.txt
-r requirements_test_pre_commit.txt
-codecov==2.1.10
+codecov==2.1.11
coverage==5.5
jsonpickle==1.4.1
mock-open==1.4.0
-mypy==0.812
+mypy==0.902
pre-commit==2.13.0
-pylint==2.8.2
+pylint==2.8.3
pipdeptree==1.0.0
pylint-strict-informational==0.1
pytest-aiohttp==0.3.0
-pytest-cov==2.10.1
+pytest-cov==2.12.1
pytest-test-groups==1.0.3
pytest-sugar==0.9.4
pytest-timeout==1.4.2
@@ -25,3 +25,19 @@ responses==0.12.0
respx==0.17.0
stdlib-list==0.7.0
tqdm==4.49.0
+types-backports==0.1.2
+types-certifi==0.1.3
+types-chardet==0.1.2
+types-cryptography==3.3.2
+types-decorator==0.1.4
+types-emoji==1.2.1
+types-enum34==0.1.5
+types-ipaddress==0.1.2
+types-jwt==0.1.3
+types-pkg-resources==0.1.2
+types-python-slugify==0.1.0
+types-pytz==0.1.1
+types-PyYAML==5.4.1
+types-requests==0.1.11
+types-toml==0.1.2
+types-ujson==0.1.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index df1f7e6dcf2..f084564b39e 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -7,7 +7,7 @@
AEMET-OpenData==0.2.1
# homeassistant.components.homekit
-HAP-python==3.5.0
+HAP-python==3.5.1
# homeassistant.components.flick_electric
PyFlick==0.0.2
@@ -48,7 +48,7 @@ abodepy==1.2.0
accuweather==0.2.0
# homeassistant.components.androidtv
-adb-shell[async]==0.3.1
+adb-shell[async]==0.3.4
# homeassistant.components.alarmdecoder
adext==0.4.2
@@ -63,16 +63,16 @@ advantage_air==0.2.1
agent-py==0.0.23
# homeassistant.components.geonetnz_quakes
-aio_geojson_geonetnz_quakes==0.12
+aio_geojson_geonetnz_quakes==0.13
# homeassistant.components.geonetnz_volcano
-aio_geojson_geonetnz_volcano==0.5
+aio_geojson_geonetnz_volcano==0.6
# homeassistant.components.nsw_rural_fire_service_feed
-aio_geojson_nsw_rfs_incidents==0.3
+aio_geojson_nsw_rfs_incidents==0.4
# homeassistant.components.gdacs
-aio_georss_gdacs==0.4
+aio_georss_gdacs==0.5
# homeassistant.components.ambient_station
aioambient==1.2.4
@@ -100,7 +100,7 @@ aioeafm==0.1.2
aioemonitor==1.0.5
# homeassistant.components.esphome
-aioesphomeapi==2.8.0
+aioesphomeapi==4.0.1
# homeassistant.components.flo
aioflo==0.4.1
@@ -112,14 +112,14 @@ aioguardian==1.0.4
aioharmony==0.2.7
# homeassistant.components.homekit_controller
-aiohomekit==0.2.67
+aiohomekit==0.4.2
# homeassistant.components.emulated_hue
# homeassistant.components.http
aiohttp_cors==0.7.0
# homeassistant.components.hue
-aiohue==2.5.0
+aiohue==2.5.1
# homeassistant.components.apache_kafka
aiokafka==0.6.0
@@ -130,6 +130,12 @@ aiolip==1.1.4
# homeassistant.components.lyric
aiolyric==1.0.7
+# homeassistant.components.modern_forms
+aiomodernforms==0.1.8
+
+# homeassistant.components.yamaha_musiccast
+aiomusiccast==0.8.0
+
# homeassistant.components.notion
aionotion==1.1.0
@@ -140,7 +146,7 @@ aiopulse==0.4.2
aiopvapi==1.6.14
# homeassistant.components.pvpc_hourly_pricing
-aiopvpc==2.1.2
+aiopvpc==2.2.0
# homeassistant.components.webostv
aiopylgtv==0.4.0
@@ -152,7 +158,7 @@ aiorecollect==1.0.5
aioshelly==0.6.4
# homeassistant.components.switcher_kis
-aioswitcher==1.2.1
+aioswitcher==1.2.3
# homeassistant.components.syncthing
aiosyncthing==0.5.1
@@ -166,11 +172,14 @@ aioymaps==1.1.0
# homeassistant.components.airly
airly==1.1.0
+# homeassistant.components.ambee
+ambee==0.3.0
+
# homeassistant.components.ambiclimate
ambiclimate==0.2.1
# homeassistant.components.androidtv
-androidtv[async]==0.0.59
+androidtv[async]==0.0.60
# homeassistant.components.apns
apns2==0.3.0
@@ -187,7 +196,7 @@ arcam-fmj==0.5.3
# homeassistant.components.dlna_dmr
# homeassistant.components.ssdp
# homeassistant.components.upnp
-async-upnp-client==0.18.0
+async-upnp-client==0.19.0
# homeassistant.components.aurora
auroranoaa==0.0.2
@@ -199,13 +208,13 @@ av==8.0.3
axis==44
# homeassistant.components.azure_event_hub
-azure-eventhub==5.1.0
+azure-eventhub==5.5.0
# homeassistant.components.homekit
base36==0.1.1
# homeassistant.components.zha
-bellows==0.24.0
+bellows==0.25.0
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.7.15
@@ -220,7 +229,7 @@ blinkpy==0.17.0
bond-api==0.1.12
# homeassistant.components.bosch_shc
-boschshcpy==0.2.17
+boschshcpy==0.2.19
# homeassistant.components.braviatv
bravia-tv==1.0.11
@@ -240,6 +249,9 @@ buienradar==1.0.4
# homeassistant.components.caldav
caldav==0.7.1
+# homeassistant.components.coinbase
+coinbase==2.1.0
+
# homeassistant.scripts.check_config
colorlog==5.0.1
@@ -258,7 +270,7 @@ coronavirus==1.1.1
datadog==0.15.0
# homeassistant.components.metoffice
-datapoint==0.9.5
+datapoint==0.9.8
# homeassistant.components.debugpy
debugpy==1.3.0
@@ -326,6 +338,9 @@ fnvhash==0.1.0
# homeassistant.components.foobot
foobot_async==1.0.0
+# homeassistant.components.forecast_solar
+forecast_solar==1.3.1
+
# homeassistant.components.freebox
freebox-api==0.0.10
@@ -334,8 +349,11 @@ freebox-api==0.0.10
# homeassistant.components.fritzbox_netmonitor
fritzconnection==1.4.2
+# homeassistant.components.fritz
+fritzprofiles==0.6.1
+
# homeassistant.components.google_translate
-gTTS==2.2.2
+gTTS==2.2.3
# homeassistant.components.garages_amsterdam
garages-amsterdam==2.1.1
@@ -351,13 +369,13 @@ geojson_client==0.6
geopy==2.1.0
# homeassistant.components.geo_rss_events
-georss_generic_client==0.4
+georss_generic_client==0.6
# homeassistant.components.ign_sismologia
-georss_ign_sismologia_client==0.2
+georss_ign_sismologia_client==0.3
# homeassistant.components.qld_bushfire
-georss_qld_bushfire_alert_client==0.3
+georss_qld_bushfire_alert_client==0.5
# homeassistant.components.huawei_lte
# homeassistant.components.kef
@@ -366,7 +384,7 @@ georss_qld_bushfire_alert_client==0.3
getmac==0.8.2
# homeassistant.components.gios
-gios==1.0.1
+gios==1.0.2
# homeassistant.components.glances
glances_api==0.2.0
@@ -387,7 +405,7 @@ google-nest-sdm==0.2.12
googlemaps==2.5.1
# homeassistant.components.gree
-greeclimate==0.11.4
+greeclimate==0.11.7
# homeassistant.components.growatt_server
growattServer==1.0.1
@@ -399,19 +417,19 @@ guppy3==3.1.0
ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
-ha-philipsjs==2.7.3
+ha-philipsjs==2.7.4
# homeassistant.components.habitica
habitipy==0.2.0
# homeassistant.components.hangouts
-hangups==0.4.11
+hangups==0.4.14
# homeassistant.components.cloud
-hass-nabucasa==0.43.0
+hass-nabucasa==0.44.0
# homeassistant.components.tasmota
-hatasmota==0.2.14
+hatasmota==0.2.19
# homeassistant.components.jewish_calendar
hdate==0.10.2
@@ -429,7 +447,7 @@ hole==0.5.1
holidays==0.11.1
# homeassistant.components.frontend
-home-assistant-frontend==20210603.0
+home-assistant-frontend==20210707.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -460,7 +478,7 @@ hyperion-py==0.7.4
iaqualink==0.3.90
# homeassistant.components.ping
-icmplib==2.1.1
+icmplib==3.0
# homeassistant.components.network
ifaddr==0.1.7
@@ -511,7 +529,7 @@ maxcube-api==0.4.3
mbddns==0.1.2
# homeassistant.components.minecraft_server
-mcstatus==5.1.1
+mcstatus==6.0.0
# homeassistant.components.meteo_france
meteofrance-api==1.0.2
@@ -519,8 +537,11 @@ meteofrance-api==1.0.2
# homeassistant.components.mfi
mficlient==0.3.0
+# homeassistant.components.xiaomi_miio
+micloud==0.3
+
# homeassistant.components.mill
-millheater==0.4.1
+millheater==0.5.0
# homeassistant.components.minio
minio==4.0.9
@@ -547,11 +568,10 @@ ndms2_client==0.1.1
nessclient==0.9.15
# homeassistant.components.discovery
-# homeassistant.components.ssdp
-netdisco==2.8.3
+netdisco==2.9.0
# homeassistant.components.nam
-nettigo-air-monitor==0.2.6
+nettigo-air-monitor==1.0.0
# homeassistant.components.nexia
nexia==0.9.7
@@ -560,7 +580,7 @@ nexia==0.9.7
notify-events==1.0.4
# homeassistant.components.nsw_fuel_station
-nsw-fuel-api-client==1.0.10
+nsw-fuel-api-client==1.1.0
# homeassistant.components.nuheat
nuheat==0.3.0
@@ -625,10 +645,10 @@ pilight==0.1.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
-pillow==8.1.2
+pillow==8.2.0
# homeassistant.components.plex
-plexapi==4.5.1
+plexapi==4.6.1
# homeassistant.components.plex
plexauth==0.0.6
@@ -653,7 +673,7 @@ poolsense==0.0.8
praw==7.2.0
# homeassistant.components.islamic_prayer_times
-prayer_times_calculator==0.0.3
+prayer_times_calculator==0.0.5
# homeassistant.components.progettihwsw
progettihwsw==0.1.1
@@ -696,7 +716,7 @@ pyMetno==0.8.3
pyRFXtrx==0.27.0
# homeassistant.components.tibber
-pyTibber==0.17.0
+pyTibber==0.18.0
# homeassistant.components.nextbus
py_nextbusnext==0.1.4
@@ -720,7 +740,7 @@ pyarlo==0.2.4
pyatag==0.3.5.3
# homeassistant.components.netatmo
-pyatmo==5.0.1
+pyatmo==5.2.0
# homeassistant.components.apple_tv
pyatv==0.7.7
@@ -729,13 +749,13 @@ pyatv==0.7.7
pyblackbird==0.5
# homeassistant.components.neato
-pybotvac==0.0.20
+pybotvac==0.0.21
# homeassistant.components.cloudflare
pycfdns==1.2.1
# homeassistant.components.cast
-pychromecast==9.1.2
+pychromecast==9.2.0
# homeassistant.components.climacell
pyclimacell==0.18.2
@@ -747,10 +767,10 @@ pycomfoconnect==0.4
pycoolmasternet-async==0.1.2
# homeassistant.components.daikin
-pydaikin==2.4.3
+pydaikin==2.4.4
# homeassistant.components.deconz
-pydeconz==79
+pydeconz==80
# homeassistant.components.dexcom
pydexcom==0.2.0
@@ -765,7 +785,7 @@ pyeconet==0.1.14
pyeverlights==0.1.0
# homeassistant.components.ezviz
-pyezviz==0.1.8.7
+pyezviz==0.1.8.9
# homeassistant.components.fido
pyfido==2.1.1
@@ -782,6 +802,9 @@ pyflunearyou==1.0.7
# homeassistant.components.forked_daapd
pyforked-daapd==0.1.11
+# homeassistant.components.freedompro
+pyfreedompro==1.1.0
+
# homeassistant.components.fritzbox
pyfritzhome==0.4.2
@@ -805,7 +828,7 @@ pyheos==0.7.2
pyhiveapi==0.4.2
# homeassistant.components.homematic
-pyhomematic==0.1.72
+pyhomematic==0.1.73
# homeassistant.components.ialarm
pyialarm==1.9.0
@@ -874,7 +897,7 @@ pymelcloud==2.5.3
pymeteoclimatic==0.0.6
# homeassistant.components.somfy
-pymfy==0.9.3
+pymfy==0.11.0
# homeassistant.components.mochad
pymochad==0.2.0
@@ -948,7 +971,7 @@ pyqwikswitch==0.93
pyrisco==0.3.1
# homeassistant.components.rituals_perfume_genie
-pyrituals==0.0.3
+pyrituals==0.0.4
# homeassistant.components.ruckus_unleashed
pyruckus==0.12
@@ -968,7 +991,7 @@ pysiaalarm==3.0.0
pysignalclirestapi==0.3.4
# homeassistant.components.sma
-pysma==0.4.3
+pysma==0.6.2
# homeassistant.components.smappee
pysmappee==0.2.25
@@ -983,7 +1006,7 @@ pysmartthings==0.7.6
pysoma==0.0.10
# homeassistant.components.sonos
-pysonos==0.0.49
+pysonos==0.0.51
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -1001,7 +1024,7 @@ python-ecobee-api==0.2.11
python-forecastio==1.4.0
# homeassistant.components.izone
-python-izone==1.1.4
+python-izone==1.1.6
# homeassistant.components.juicenet
python-juicenet==1.0.2
@@ -1037,7 +1060,7 @@ python-velbus==2.1.2
python_awair==0.2.1
# homeassistant.components.tile
-pytile==5.2.0
+pytile==5.2.2
# homeassistant.components.traccar
pytraccar==0.9.0
@@ -1125,7 +1148,7 @@ sharkiqpy==0.1.8
simplehound==0.3
# homeassistant.components.simplisafe
-simplisafe-python==10.0.0
+simplisafe-python==11.0.0
# homeassistant.components.slack
slackclient==2.5.0
@@ -1168,7 +1191,7 @@ spotipy==2.18.0
# homeassistant.components.recorder
# homeassistant.components.sql
-sqlalchemy==1.4.13
+sqlalchemy==1.4.17
# homeassistant.components.srp_energy
srpenergy==1.3.2
@@ -1263,7 +1286,7 @@ wakeonlan==2.0.1
wallbox==0.4.4
# homeassistant.components.folder_watcher
-watchdog==2.1.2
+watchdog==2.1.3
# homeassistant.components.wiffi
wiffi==1.0.1
@@ -1272,18 +1295,19 @@ wiffi==1.0.1
withings-api==2.3.2
# homeassistant.components.wled
-wled==0.4.4
+wled==0.7.1
# homeassistant.components.wolflink
-wolf_smartset==0.1.8
+wolf_smartset==0.1.11
# homeassistant.components.xbox
xbox-webapi==2.0.11
# homeassistant.components.knx
-xknx==0.18.4
+xknx==0.18.8
# homeassistant.components.bluesound
+# homeassistant.components.fritz
# homeassistant.components.rest
# homeassistant.components.startca
# homeassistant.components.ted5000
@@ -1300,10 +1324,10 @@ yeelight==0.6.3
zeep[async]==4.0.0
# homeassistant.components.zeroconf
-zeroconf==0.31.0
+zeroconf==0.32.1
# homeassistant.components.zha
-zha-quirks==0.0.57
+zha-quirks==0.0.59
# homeassistant.components.zha
zigpy-cc==0.5.2
@@ -1321,7 +1345,7 @@ zigpy-zigate==0.7.3
zigpy-znp==0.5.1
# homeassistant.components.zha
-zigpy==0.33.0
+zigpy==0.35.1
# homeassistant.components.zwave_js
-zwave-js-server-python==0.26.1
+zwave-js-server-python==0.27.0
diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt
index e403e05fcfd..3f473ae1592 100644
--- a/requirements_test_pre_commit.txt
+++ b/requirements_test_pre_commit.txt
@@ -1,7 +1,7 @@
# Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit
bandit==1.7.0
-black==21.5b1
+black==21.6b0
codespell==2.0.0
flake8-comprehensions==3.5.0
flake8-docstrings==1.6.0
diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py
index a8e1858cad3..00110e11fbc 100644
--- a/script/hassfest/manifest.py
+++ b/script/hassfest/manifest.py
@@ -91,6 +91,7 @@ NO_IOT_CLASS = [
"scene",
"script",
"search",
+ "select",
"sensor",
"stt",
"switch",
diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py
index 6310d0117c5..601e6b55845 100644
--- a/script/hassfest/mypy_config.py
+++ b/script/hassfest/mypy_config.py
@@ -46,7 +46,6 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.dhcp.*",
"homeassistant.components.directv.*",
"homeassistant.components.doorbird.*",
- "homeassistant.components.dsmr.*",
"homeassistant.components.dynalite.*",
"homeassistant.components.eafm.*",
"homeassistant.components.edl21.*",
@@ -85,7 +84,12 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.hisense_aehw4a1.*",
"homeassistant.components.home_connect.*",
"homeassistant.components.home_plus_control.*",
- "homeassistant.components.homeassistant.*",
+ "homeassistant.components.homeassistant.triggers.homeassistant",
+ "homeassistant.components.homeassistant.triggers.numeric_state",
+ "homeassistant.components.homeassistant.triggers.time_pattern",
+ "homeassistant.components.homeassistant.triggers.time",
+ "homeassistant.components.homeassistant.triggers.state",
+ "homeassistant.components.homeassistant.scene",
"homeassistant.components.homekit.*",
"homeassistant.components.homekit_controller.*",
"homeassistant.components.homematicip_cloud.*",
@@ -97,7 +101,6 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.image.*",
"homeassistant.components.incomfort.*",
"homeassistant.components.influxdb.*",
- "homeassistant.components.input_boolean.*",
"homeassistant.components.input_datetime.*",
"homeassistant.components.input_number.*",
"homeassistant.components.insteon.*",
@@ -127,7 +130,6 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.motion_blinds.*",
"homeassistant.components.mqtt.*",
"homeassistant.components.mullvad.*",
- "homeassistant.components.mysensors.*",
"homeassistant.components.neato.*",
"homeassistant.components.ness_alarm.*",
"homeassistant.components.nest.*",
diff --git a/script/run-in-env.sh b/script/run-in-env.sh
index 0f531f235b6..271e7a4a034 100755
--- a/script/run-in-env.sh
+++ b/script/run-in-env.sh
@@ -15,6 +15,7 @@ my_path=$(git rev-parse --show-toplevel)
for venv in venv .venv .; do
if [ -f "${my_path}/${venv}/bin/activate" ]; then
. "${my_path}/${venv}/bin/activate"
+ break
fi
done
diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py
index f88390599e7..cf6dfd9cc20 100644
--- a/script/scaffold/templates/config_flow/integration/config_flow.py
+++ b/script/scaffold/templates/config_flow/integration/config_flow.py
@@ -16,7 +16,13 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
# TODO adjust the data schema to the data that you need
-STEP_USER_DATA_SCHEMA = vol.Schema({"host": str, "username": str, "password": str})
+STEP_USER_DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required("host"): str,
+ vol.Required("username"): str,
+ vol.Required("password"): str,
+ }
+)
class PlaceholderHub:
diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py
index 674cb921cdd..e72d9eb7679 100644
--- a/script/scaffold/templates/config_flow/tests/test_config_flow.py
+++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py
@@ -5,6 +5,7 @@ from homeassistant import config_entries, setup
from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth
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
async def test_form(hass: HomeAssistant) -> None:
@@ -13,7 +14,7 @@ async def test_form(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- assert result["type"] == "form"
+ assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
@@ -33,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None:
)
await hass.async_block_till_done()
- assert result2["type"] == "create_entry"
+ assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "Name of the device"
assert result2["data"] == {
"host": "1.1.1.1",
@@ -62,7 +63,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
},
)
- assert result2["type"] == "form"
+ assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "invalid_auth"}
@@ -85,5 +86,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
},
)
- assert result2["type"] == "form"
+ assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "cannot_connect"}
diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py
index 3bd1c0b91b3..720e472851c 100644
--- a/script/scaffold/templates/device_action/integration/device_action.py
+++ b/script/scaffold/templates/device_action/integration/device_action.py
@@ -48,22 +48,14 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
# Add actions for each entity that belongs to this integration
# TODO add your own actions.
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "turn_on",
- }
- )
- actions.append(
- {
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "turn_off",
- }
- )
+ base_action = {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
+ actions.append({**base_action, CONF_TYPE: "turn_on"})
+ actions.append({**base_action, CONF_TYPE: "turn_off"})
return actions
diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py
index 413812828e5..4180f81b3ff 100644
--- a/script/scaffold/templates/device_condition/integration/device_condition.py
+++ b/script/scaffold/templates/device_condition/integration/device_condition.py
@@ -45,24 +45,14 @@ async def async_get_conditions(
# Add conditions for each entity that belongs to this integration
# TODO add your own conditions.
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_on",
- }
- )
- conditions.append(
- {
- CONF_CONDITION: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "is_off",
- }
- )
+ base_condition = {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
+ conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES]
return conditions
diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py
index ca430368544..e070bc43f57 100644
--- a/script/scaffold/templates/device_trigger/integration/device_trigger.py
+++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import state
from homeassistant.const import (
CONF_DEVICE_ID,
@@ -24,7 +24,7 @@ from . import DOMAIN
# TODO specify your supported trigger types.
TRIGGER_TYPES = {"turned_on", "turned_off"}
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
@@ -51,24 +51,14 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
# Add triggers for each entity that belongs to this integration
# TODO add your own triggers.
- triggers.append(
- {
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "turned_on",
- }
- )
- triggers.append(
- {
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "turned_off",
- }
- )
+ base_trigger = {
+ CONF_PLATFORM: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+ triggers.append({**base_trigger, CONF_TYPE: "turned_on"})
+ triggers.append({**base_trigger, CONF_TYPE: "turned_off"})
return triggers
diff --git a/setup.py b/setup.py
index 0178b201372..758f4f3813d 100755
--- a/setup.py
+++ b/setup.py
@@ -42,7 +42,7 @@ REQUIRES = [
"certifi>=2020.12.5",
"ciso8601==2.1.3",
"httpx==0.18.0",
- "jinja2>=3.0.1",
+ "jinja2==3.0.1",
"PyJWT==1.7.1",
# PyJWT has loose dependency. We want the latest one.
"cryptography==3.3.2",
diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py
index 39764fa4206..d7574bf0da1 100644
--- a/tests/auth/providers/test_trusted_networks.py
+++ b/tests/auth/providers/test_trusted_networks.py
@@ -8,6 +8,9 @@ import voluptuous as vol
from homeassistant import auth
from homeassistant.auth import auth_store
from homeassistant.auth.providers import trusted_networks as tn_auth
+from homeassistant.components.http import CONF_TRUSTED_PROXIES, CONF_USE_X_FORWARDED_FOR
+from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY
+from homeassistant.setup import async_setup_component
@pytest.fixture
@@ -143,6 +146,29 @@ async def test_validate_access(provider):
provider.async_validate_access(ip_address("2001:db8::ff00:42:8329"))
+async def test_validate_access_proxy(hass, provider):
+ """Test validate access from trusted networks are blocked from proxy."""
+
+ await async_setup_component(
+ hass,
+ "http",
+ {
+ "http": {
+ CONF_TRUSTED_PROXIES: ["192.168.128.0/31", "fd00::1"],
+ CONF_USE_X_FORWARDED_FOR: True,
+ }
+ },
+ )
+ provider.async_validate_access(ip_address("192.168.128.2"))
+ provider.async_validate_access(ip_address("fd00::2"))
+ with pytest.raises(tn_auth.InvalidAuthError):
+ provider.async_validate_access(ip_address("192.168.128.0"))
+ with pytest.raises(tn_auth.InvalidAuthError):
+ provider.async_validate_access(ip_address("192.168.128.1"))
+ with pytest.raises(tn_auth.InvalidAuthError):
+ provider.async_validate_access(ip_address("fd00::1"))
+
+
async def test_validate_refresh_token(provider):
"""Verify re-validation of refresh token."""
with patch.object(provider, "async_validate_access") as mock:
@@ -161,7 +187,7 @@ async def test_login_flow(manager, provider):
# not from trusted network
flow = await provider.async_login_flow({"ip_address": ip_address("127.0.0.1")})
step = await flow.async_step_init()
- assert step["type"] == "abort"
+ assert step["type"] == RESULT_TYPE_ABORT
assert step["reason"] == "not_allowed"
# from trusted network, list users
@@ -176,7 +202,7 @@ async def test_login_flow(manager, provider):
# login with valid user
step = await flow.async_step_init({"user": user.id})
- assert step["type"] == "create_entry"
+ assert step["type"] == RESULT_TYPE_CREATE_ENTRY
assert step["data"]["user"] == user.id
@@ -200,7 +226,7 @@ async def test_trusted_users_login(manager_with_user, provider_with_user):
{"ip_address": ip_address("127.0.0.1")}
)
step = await flow.async_step_init()
- assert step["type"] == "abort"
+ assert step["type"] == RESULT_TYPE_ABORT
assert step["reason"] == "not_allowed"
# from trusted network, list users intersect trusted_users
@@ -284,7 +310,7 @@ async def test_trusted_group_login(manager_with_user, provider_with_user):
{"ip_address": ip_address("127.0.0.1")}
)
step = await flow.async_step_init()
- assert step["type"] == "abort"
+ assert step["type"] == RESULT_TYPE_ABORT
assert step["reason"] == "not_allowed"
# from trusted network, list users intersect trusted_users
@@ -322,7 +348,7 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login):
{"ip_address": ip_address("127.0.0.1")}
)
step = await flow.async_step_init()
- assert step["type"] == "abort"
+ assert step["type"] == RESULT_TYPE_ABORT
assert step["reason"] == "not_allowed"
# from trusted network, only one available user, bypass the login flow
@@ -330,7 +356,7 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login):
{"ip_address": ip_address("192.168.0.1")}
)
step = await flow.async_step_init()
- assert step["type"] == "create_entry"
+ assert step["type"] == RESULT_TYPE_CREATE_ENTRY
assert step["data"]["user"] == owner.id
user = await manager_bypass_login.async_create_user("test-user")
diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py
index 255fcac7694..0128c9794f3 100644
--- a/tests/auth/test_init.py
+++ b/tests/auth/test_init.py
@@ -1001,3 +1001,21 @@ async def test_new_users(mock_hass):
)
)
assert user_cred.is_admin
+
+
+async def test_rename_does_not_change_refresh_token(mock_hass):
+ """Test that we can rename without changing refresh token."""
+ manager = await auth.auth_manager_from_config(mock_hass, [], [])
+ user = MockUser().add_to_auth_manager(manager)
+ await manager.async_create_refresh_token(user, CLIENT_ID)
+
+ assert len(list(user.refresh_tokens.values())) == 1
+ token_before = list(user.refresh_tokens.values())[0]
+
+ await manager.async_update_user(user, name="new name")
+ assert user.name == "new name"
+
+ assert len(list(user.refresh_tokens.values())) == 1
+ token_after = list(user.refresh_tokens.values())[0]
+
+ assert token_before == token_after
diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py
index 64c49c61fe7..62f282f2cf3 100644
--- a/tests/components/accuweather/test_sensor.py
+++ b/tests/components/accuweather/test_sensor.py
@@ -4,7 +4,11 @@ import json
from unittest.mock import PropertyMock, patch
from homeassistant.components.accuweather.const import ATTRIBUTION, DOMAIN
-from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.sensor import (
+ ATTR_STATE_CLASS,
+ DOMAIN as SENSOR_DOMAIN,
+ STATE_CLASS_MEASUREMENT,
+)
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
@@ -43,6 +47,7 @@ async def test_sensor_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_METERS
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_cloud_ceiling")
assert entry
@@ -55,6 +60,7 @@ async def test_sensor_without_forecast(hass):
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILLIMETERS
assert state.attributes.get(ATTR_ICON) == "mdi:weather-rainy"
assert state.attributes.get("type") is None
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_precipitation")
assert entry
@@ -66,6 +72,7 @@ async def test_sensor_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:gauge"
assert state.attributes.get(ATTR_DEVICE_CLASS) == "accuweather__pressure_tendency"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_pressure_tendency")
assert entry
@@ -77,6 +84,7 @@ async def test_sensor_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_realfeel_temperature")
assert entry
@@ -88,6 +96,7 @@ async def test_sensor_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX
assert state.attributes.get("level") == "High"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_uv_index")
assert entry
@@ -105,6 +114,7 @@ async def test_sensor_with_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-partly-cloudy"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_HOURS
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_hours_of_sun_0d")
assert entry
@@ -116,6 +126,7 @@ async def test_sensor_with_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_realfeel_temperature_max_0d")
assert entry
@@ -126,6 +137,7 @@ async def test_sensor_with_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_realfeel_temperature_min_0d")
assert entry
@@ -137,6 +149,7 @@ async def test_sensor_with_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_thunderstorm_probability_day_0d")
assert entry
@@ -148,6 +161,7 @@ async def test_sensor_with_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_thunderstorm_probability_night_0d")
assert entry
@@ -160,6 +174,7 @@ async def test_sensor_with_forecast(hass):
assert state.attributes.get(ATTR_ICON) == "mdi:weather-sunny"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX
assert state.attributes.get("level") == "Moderate"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_uv_index_0d")
assert entry
@@ -346,6 +361,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_apparent_temperature")
assert entry
@@ -357,6 +373,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_cloud_cover")
assert entry
@@ -368,6 +385,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_dew_point")
assert entry
@@ -379,6 +397,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_realfeel_temperature_shade")
assert entry
@@ -390,6 +409,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_wet_bulb_temperature")
assert entry
@@ -401,6 +421,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_wind_chill_temperature")
assert entry
@@ -412,6 +433,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR
assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_wind_gust")
assert entry
@@ -423,6 +445,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR
assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_wind")
assert entry
@@ -434,6 +457,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_cloud_cover_day_0d")
assert entry
@@ -445,6 +469,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_cloud_cover_night_0d")
assert entry
@@ -459,6 +484,7 @@ async def test_sensor_enabled_without_forecast(hass):
)
assert state.attributes.get("level") == "Low"
assert state.attributes.get(ATTR_ICON) == "mdi:grass"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_grass_pollen_0d")
assert entry
@@ -485,6 +511,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get("level") == "Good"
assert state.attributes.get(ATTR_ICON) == "mdi:vector-triangle"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_ozone_0d")
assert entry
@@ -511,6 +538,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_realfeel_temperature_shade_max_0d")
assert entry
@@ -537,6 +565,7 @@ async def test_sensor_enabled_without_forecast(hass):
)
assert state.attributes.get("level") == "Low"
assert state.attributes.get(ATTR_ICON) == "mdi:tree-outline"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_tree_pollen_0d")
assert entry
@@ -561,6 +590,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR
assert state.attributes.get("direction") == "WNW"
assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_wind_night_0d")
assert entry
@@ -573,6 +603,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR
assert state.attributes.get("direction") == "S"
assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_wind_gust_day_0d")
assert entry
@@ -585,6 +616,7 @@ async def test_sensor_enabled_without_forecast(hass):
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR
assert state.attributes.get("direction") == "WSW"
assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.home_wind_gust_night_0d")
assert entry
diff --git a/tests/components/accuweather/test_system_health.py b/tests/components/accuweather/test_system_health.py
index 749f516e44c..bbdee3cb6f9 100644
--- a/tests/components/accuweather/test_system_health.py
+++ b/tests/components/accuweather/test_system_health.py
@@ -4,7 +4,7 @@ from unittest.mock import Mock
from aiohttp import ClientError
-from homeassistant.components.accuweather.const import COORDINATOR, DOMAIN
+from homeassistant.components.accuweather.const import DOMAIN
from homeassistant.setup import async_setup_component
from tests.common import get_system_health_info
@@ -18,9 +18,7 @@ async def test_accuweather_system_health(hass, aioclient_mock):
hass.data[DOMAIN] = {}
hass.data[DOMAIN]["0123xyz"] = {}
- hass.data[DOMAIN]["0123xyz"][COORDINATOR] = Mock(
- accuweather=Mock(requests_remaining="42")
- )
+ hass.data[DOMAIN]["0123xyz"] = Mock(accuweather=Mock(requests_remaining="42"))
info = await get_system_health_info(hass, DOMAIN)
@@ -42,9 +40,7 @@ async def test_accuweather_system_health_fail(hass, aioclient_mock):
hass.data[DOMAIN] = {}
hass.data[DOMAIN]["0123xyz"] = {}
- hass.data[DOMAIN]["0123xyz"][COORDINATOR] = Mock(
- accuweather=Mock(requests_remaining="0")
- )
+ hass.data[DOMAIN]["0123xyz"] = Mock(accuweather=Mock(requests_remaining="0"))
info = await get_system_health_info(hass, DOMAIN)
diff --git a/tests/components/airly/test_air_quality.py b/tests/components/airly/test_air_quality.py
deleted file mode 100644
index de059e84aa4..00000000000
--- a/tests/components/airly/test_air_quality.py
+++ /dev/null
@@ -1,114 +0,0 @@
-"""Test air_quality of Airly integration."""
-from datetime import timedelta
-
-from airly.exceptions import AirlyError
-
-from homeassistant.components.air_quality import ATTR_AQI, ATTR_PM_2_5, ATTR_PM_10
-from homeassistant.components.airly.air_quality import (
- ATTRIBUTION,
- LABEL_ADVICE,
- LABEL_AQI_DESCRIPTION,
- LABEL_AQI_LEVEL,
- LABEL_PM_2_5_LIMIT,
- LABEL_PM_2_5_PERCENT,
- LABEL_PM_10_LIMIT,
- LABEL_PM_10_PERCENT,
-)
-from homeassistant.const import (
- ATTR_ATTRIBUTION,
- ATTR_ENTITY_ID,
- ATTR_ICON,
- ATTR_UNIT_OF_MEASUREMENT,
- CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
- HTTP_INTERNAL_SERVER_ERROR,
- STATE_UNAVAILABLE,
-)
-from homeassistant.helpers import entity_registry as er
-from homeassistant.setup import async_setup_component
-from homeassistant.util.dt import utcnow
-
-from . import API_POINT_URL
-
-from tests.common import async_fire_time_changed, load_fixture
-from tests.components.airly import init_integration
-
-
-async def test_air_quality(hass, aioclient_mock):
- """Test states of the air_quality."""
- await init_integration(hass, aioclient_mock)
- registry = er.async_get(hass)
-
- state = hass.states.get("air_quality.home")
- assert state
- assert state.state == "14"
- assert state.attributes.get(ATTR_AQI) == 23
- assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
- assert state.attributes.get(LABEL_ADVICE) == "Great air!"
- assert state.attributes.get(ATTR_PM_10) == 19
- assert state.attributes.get(ATTR_PM_2_5) == 14
- assert state.attributes.get(LABEL_AQI_DESCRIPTION) == "Great air here today!"
- assert state.attributes.get(LABEL_AQI_LEVEL) == "very low"
- assert state.attributes.get(LABEL_PM_2_5_LIMIT) == 25.0
- assert state.attributes.get(LABEL_PM_2_5_PERCENT) == 55
- assert state.attributes.get(LABEL_PM_10_LIMIT) == 50.0
- assert state.attributes.get(LABEL_PM_10_PERCENT) == 37
- assert (
- state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
- == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
- )
- assert state.attributes.get(ATTR_ICON) == "mdi:blur"
-
- entry = registry.async_get("air_quality.home")
- assert entry
- assert entry.unique_id == "123-456"
-
-
-async def test_availability(hass, aioclient_mock):
- """Ensure that we mark the entities unavailable correctly when service causes an error."""
- await init_integration(hass, aioclient_mock)
-
- state = hass.states.get("air_quality.home")
- assert state
- assert state.state != STATE_UNAVAILABLE
- assert state.state == "14"
-
- aioclient_mock.clear_requests()
- aioclient_mock.get(
- API_POINT_URL, exc=AirlyError(HTTP_INTERNAL_SERVER_ERROR, "Unexpected error")
- )
- future = utcnow() + timedelta(minutes=60)
-
- async_fire_time_changed(hass, future)
- await hass.async_block_till_done()
-
- state = hass.states.get("air_quality.home")
- assert state
- assert state.state == STATE_UNAVAILABLE
-
- aioclient_mock.clear_requests()
- aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json"))
- future = utcnow() + timedelta(minutes=120)
-
- async_fire_time_changed(hass, future)
- await hass.async_block_till_done()
-
- state = hass.states.get("air_quality.home")
- assert state
- assert state.state != STATE_UNAVAILABLE
- assert state.state == "14"
-
-
-async def test_manual_update_entity(hass, aioclient_mock):
- """Test manual update entity via service homeasasistant/update_entity."""
- await init_integration(hass, aioclient_mock)
-
- call_count = aioclient_mock.call_count
- await async_setup_component(hass, "homeassistant", {})
- await hass.services.async_call(
- "homeassistant",
- "update_entity",
- {ATTR_ENTITY_ID: ["air_quality.home"]},
- blocking=True,
- )
-
- assert aioclient_mock.call_count == call_count + 1
diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py
index a20ae6ddd1a..252c01c124a 100644
--- a/tests/components/airly/test_init.py
+++ b/tests/components/airly/test_init.py
@@ -3,10 +3,12 @@ from unittest.mock import patch
import pytest
+from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
from homeassistant.components.airly import set_update_interval
from homeassistant.components.airly.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.helpers import entity_registry as er
from homeassistant.util.dt import utcnow
from . import API_POINT_URL
@@ -24,7 +26,7 @@ async def test_async_setup_entry(hass, aioclient_mock):
"""Test a successful setup entry."""
await init_integration(hass, aioclient_mock)
- state = hass.states.get("air_quality.home")
+ state = hass.states.get("sensor.home_pm2_5")
assert state is not None
assert state.state != STATE_UNAVAILABLE
assert state.state == "14"
@@ -216,3 +218,21 @@ async def test_migrate_device_entry(hass, aioclient_mock, old_identifier):
config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "123-456")}
)
assert device_entry.id == migrated_device_entry.id
+
+
+async def test_remove_air_quality_entities(hass, aioclient_mock):
+ """Test remove air_quality entities from registry."""
+ registry = er.async_get(hass)
+
+ registry.async_get_or_create(
+ AIR_QUALITY_PLATFORM,
+ DOMAIN,
+ "123-456",
+ suggested_object_id="home",
+ disabled_by=None,
+ )
+
+ await init_integration(hass, aioclient_mock)
+
+ entry = registry.async_get("air_quality.home")
+ assert entry is None
diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py
index 925f3acb6d2..c566702a5b4 100644
--- a/tests/components/airly/test_sensor.py
+++ b/tests/components/airly/test_sensor.py
@@ -2,6 +2,7 @@
from datetime import timedelta
from homeassistant.components.airly.sensor import ATTRIBUTION
+from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
@@ -32,12 +33,23 @@ async def test_sensor(hass, aioclient_mock):
await init_integration(hass, aioclient_mock)
registry = er.async_get(hass)
+ state = hass.states.get("sensor.home_caqi")
+ assert state
+ assert state.state == "23"
+ assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "CAQI"
+
+ entry = registry.async_get("sensor.home_caqi")
+ assert entry
+ assert entry.unique_id == "123-456-caqi"
+
state = hass.states.get("sensor.home_humidity")
assert state
assert state.state == "92.8"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_humidity")
assert entry
@@ -52,17 +64,49 @@ async def test_sensor(hass, aioclient_mock):
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
)
assert state.attributes.get(ATTR_ICON) == "mdi:blur"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_pm1")
assert entry
assert entry.unique_id == "123-456-pm1"
+ state = hass.states.get("sensor.home_pm2_5")
+ assert state
+ assert state.state == "14"
+ assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:blur"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+
+ entry = registry.async_get("sensor.home_pm2_5")
+ assert entry
+ assert entry.unique_id == "123-456-pm25"
+
+ state = hass.states.get("sensor.home_pm10")
+ assert state
+ assert state.state == "19"
+ assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:blur"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+
+ entry = registry.async_get("sensor.home_pm10")
+ assert entry
+ assert entry.unique_id == "123-456-pm10"
+
state = hass.states.get("sensor.home_pressure")
assert state
assert state.state == "1001"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_pressure")
assert entry
@@ -74,6 +118,7 @@ async def test_sensor(hass, aioclient_mock):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.home_temperature")
assert entry
diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py
index a4ec802f3f5..1f66d901603 100644
--- a/tests/components/alarm_control_panel/test_device_action.py
+++ b/tests/components/alarm_control_panel/test_device_action.py
@@ -1,7 +1,7 @@
"""The tests for Alarm control panel device actions."""
import pytest
-from homeassistant.components.alarm_control_panel import DOMAIN
+from homeassistant.components.alarm_control_panel import DOMAIN, const
import homeassistant.components.automation as automation
from homeassistant.const import (
CONF_PLATFORM,
@@ -38,7 +38,30 @@ def entity_reg(hass):
return mock_registry(hass)
-async def test_get_actions(hass, device_reg, entity_reg):
+@pytest.mark.parametrize(
+ "set_state,features_reg,features_state,expected_action_types",
+ [
+ (False, 0, 0, ["disarm"]),
+ (False, const.SUPPORT_ALARM_ARM_AWAY, 0, ["disarm", "arm_away"]),
+ (False, const.SUPPORT_ALARM_ARM_HOME, 0, ["disarm", "arm_home"]),
+ (False, const.SUPPORT_ALARM_ARM_NIGHT, 0, ["disarm", "arm_night"]),
+ (False, const.SUPPORT_ALARM_TRIGGER, 0, ["disarm", "trigger"]),
+ (True, 0, 0, ["disarm"]),
+ (True, 0, const.SUPPORT_ALARM_ARM_AWAY, ["disarm", "arm_away"]),
+ (True, 0, const.SUPPORT_ALARM_ARM_HOME, ["disarm", "arm_home"]),
+ (True, 0, const.SUPPORT_ALARM_ARM_NIGHT, ["disarm", "arm_night"]),
+ (True, 0, const.SUPPORT_ALARM_TRIGGER, ["disarm", "trigger"]),
+ ],
+)
+async def test_get_actions(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ features_reg,
+ features_state,
+ expected_action_types,
+):
"""Test we get the expected actions from a alarm_control_panel."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@@ -46,41 +69,26 @@ async def test_get_actions(hass, device_reg, entity_reg):
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- hass.states.async_set(
- "alarm_control_panel.test_5678", "attributes", {"supported_features": 15}
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ supported_features=features_reg,
)
- expected_actions = [
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state}
+ )
+ expected_actions = []
+ expected_actions += [
{
"domain": DOMAIN,
- "type": "arm_away",
+ "type": action,
"device_id": device_entry.id,
- "entity_id": "alarm_control_panel.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "arm_home",
- "device_id": device_entry.id,
- "entity_id": "alarm_control_panel.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "arm_night",
- "device_id": device_entry.id,
- "entity_id": "alarm_control_panel.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "disarm",
- "device_id": device_entry.id,
- "entity_id": "alarm_control_panel.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "trigger",
- "device_id": device_entry.id,
- "entity_id": "alarm_control_panel.test_5678",
- },
+ "entity_id": f"{DOMAIN}.test_5678",
+ }
+ for action in expected_action_types
]
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert_lists_same(actions, expected_actions)
diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py
index 450393135af..b1e2c171cea 100644
--- a/tests/components/alarm_control_panel/test_device_condition.py
+++ b/tests/components/alarm_control_panel/test_device_condition.py
@@ -1,7 +1,7 @@
"""The tests for Alarm control panel device conditions."""
import pytest
-from homeassistant.components.alarm_control_panel import DOMAIN
+from homeassistant.components.alarm_control_panel import DOMAIN, const
import homeassistant.components.automation as automation
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
@@ -43,7 +43,30 @@ def calls(hass):
return async_mock_service(hass, "test", "automation")
-async def test_get_no_conditions(hass, device_reg, entity_reg):
+@pytest.mark.parametrize(
+ "set_state,features_reg,features_state,expected_condition_types",
+ [
+ (False, 0, 0, []),
+ (False, const.SUPPORT_ALARM_ARM_AWAY, 0, ["is_armed_away"]),
+ (False, const.SUPPORT_ALARM_ARM_HOME, 0, ["is_armed_home"]),
+ (False, const.SUPPORT_ALARM_ARM_NIGHT, 0, ["is_armed_night"]),
+ (False, const.SUPPORT_ALARM_ARM_CUSTOM_BYPASS, 0, ["is_armed_custom_bypass"]),
+ (True, 0, 0, []),
+ (True, 0, const.SUPPORT_ALARM_ARM_AWAY, ["is_armed_away"]),
+ (True, 0, const.SUPPORT_ALARM_ARM_HOME, ["is_armed_home"]),
+ (True, 0, const.SUPPORT_ALARM_ARM_NIGHT, ["is_armed_night"]),
+ (True, 0, const.SUPPORT_ALARM_ARM_CUSTOM_BYPASS, ["is_armed_custom_bypass"]),
+ ],
+)
+async def test_get_conditions(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ features_reg,
+ features_state,
+ expected_condition_types,
+):
"""Test we get the expected conditions from a alarm_control_panel."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@@ -51,101 +74,41 @@ async def test_get_no_conditions(hass, device_reg, entity_reg):
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- conditions = await async_get_device_automations(hass, "condition", device_entry.id)
- assert_lists_same(conditions, [])
-
-
-async def test_get_minimum_conditions(hass, device_reg, entity_reg):
- """Test we get the expected conditions from a alarm_control_panel."""
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ supported_features=features_reg,
)
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- hass.states.async_set(
- "alarm_control_panel.test_5678", "attributes", {"supported_features": 0}
- )
- expected_conditions = [
+ if set_state:
+ hass.states.async_set(
+ "alarm_control_panel.test_5678",
+ "attributes",
+ {"supported_features": features_state},
+ )
+ expected_conditions = []
+ basic_condition_types = ["is_disarmed", "is_triggered"]
+ expected_conditions += [
{
"condition": "device",
"domain": DOMAIN,
- "type": "is_disarmed",
+ "type": condition,
"device_id": device_entry.id,
"entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_triggered",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
+ }
+ for condition in basic_condition_types
]
-
- conditions = await async_get_device_automations(hass, "condition", device_entry.id)
- assert_lists_same(conditions, expected_conditions)
-
-
-async def test_get_maximum_conditions(hass, device_reg, entity_reg):
- """Test we get the expected conditions from a alarm_control_panel."""
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- hass.states.async_set(
- "alarm_control_panel.test_5678", "attributes", {"supported_features": 31}
- )
- expected_conditions = [
+ expected_conditions += [
{
"condition": "device",
"domain": DOMAIN,
- "type": "is_disarmed",
+ "type": condition,
"device_id": device_entry.id,
"entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_triggered",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_armed_home",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_armed_away",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_armed_night",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_armed_custom_bypass",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
+ }
+ for condition in expected_condition_types
]
-
conditions = await async_get_device_automations(hass, "condition", device_entry.id)
assert_lists_same(conditions, expected_conditions)
diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py
index 3380cdd9654..8859915b911 100644
--- a/tests/components/alarm_control_panel/test_device_trigger.py
+++ b/tests/components/alarm_control_panel/test_device_trigger.py
@@ -48,7 +48,48 @@ def calls(hass):
return async_mock_service(hass, "test", "automation")
-async def test_get_triggers(hass, device_reg, entity_reg):
+@pytest.mark.parametrize(
+ "set_state,features_reg,features_state,expected_trigger_types",
+ [
+ (False, 0, 0, ["triggered", "disarmed", "arming"]),
+ (
+ False,
+ 15,
+ 0,
+ [
+ "triggered",
+ "disarmed",
+ "arming",
+ "armed_home",
+ "armed_away",
+ "armed_night",
+ ],
+ ),
+ (True, 0, 0, ["triggered", "disarmed", "arming"]),
+ (
+ True,
+ 0,
+ 15,
+ [
+ "triggered",
+ "disarmed",
+ "arming",
+ "armed_home",
+ "armed_away",
+ "armed_night",
+ ],
+ ),
+ ],
+)
+async def test_get_triggers(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ features_reg,
+ features_state,
+ expected_trigger_types,
+):
"""Test we get the expected triggers from an alarm_control_panel."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@@ -56,53 +97,30 @@ async def test_get_triggers(hass, device_reg, entity_reg):
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- hass.states.async_set(
- "alarm_control_panel.test_5678", "attributes", {"supported_features": 15}
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ supported_features=features_reg,
)
- expected_triggers = [
+ if set_state:
+ hass.states.async_set(
+ "alarm_control_panel.test_5678",
+ "attributes",
+ {"supported_features": features_state},
+ )
+ expected_triggers = []
+
+ expected_triggers += [
{
"platform": "device",
"domain": DOMAIN,
- "type": "disarmed",
+ "type": trigger,
"device_id": device_entry.id,
"entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "triggered",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "arming",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "armed_home",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "armed_away",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "armed_night",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
+ }
+ for trigger in expected_trigger_types
]
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
assert_lists_same(triggers, expected_triggers)
diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py
index 020b03cc862..92951d4a0e7 100644
--- a/tests/components/alexa/test_capabilities.py
+++ b/tests/components/alexa/test_capabilities.py
@@ -400,6 +400,48 @@ async def test_report_fan_speed_state(hass):
properties.assert_equal("Alexa.RangeController", "rangeValue", 3)
+async def test_report_fan_preset_mode(hass):
+ """Test ModeController reports fan preset_mode correctly."""
+ hass.states.async_set(
+ "fan.preset_mode",
+ "eco",
+ {
+ "friendly_name": "eco enabled fan",
+ "supported_features": 8,
+ "preset_mode": "eco",
+ "preset_modes": ["eco", "smart", "whoosh"],
+ },
+ )
+ properties = await reported_properties(hass, "fan.preset_mode")
+ properties.assert_equal("Alexa.ModeController", "mode", "preset_mode.eco")
+
+ hass.states.async_set(
+ "fan.preset_mode",
+ "smart",
+ {
+ "friendly_name": "smart enabled fan",
+ "supported_features": 8,
+ "preset_mode": "smart",
+ "preset_modes": ["eco", "smart", "whoosh"],
+ },
+ )
+ properties = await reported_properties(hass, "fan.preset_mode")
+ properties.assert_equal("Alexa.ModeController", "mode", "preset_mode.smart")
+
+ hass.states.async_set(
+ "fan.preset_mode",
+ "whoosh",
+ {
+ "friendly_name": "whoosh enabled fan",
+ "supported_features": 8,
+ "preset_mode": "whoosh",
+ "preset_modes": ["eco", "smart", "whoosh"],
+ },
+ )
+ properties = await reported_properties(hass, "fan.preset_mode")
+ properties.assert_equal("Alexa.ModeController", "mode", "preset_mode.whoosh")
+
+
async def test_report_fan_oscillating(hass):
"""Test ToggleController reports fan oscillating correctly."""
hass.states.async_set(
diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py
index 83abe2326d7..0da21042049 100644
--- a/tests/components/alexa/test_smart_home.py
+++ b/tests/components/alexa/test_smart_home.py
@@ -846,6 +846,89 @@ async def test_fan_range_off(hass):
)
+async def test_preset_mode_fan(hass, caplog):
+ """Test fan discovery.
+
+ This one has preset modes.
+ """
+ device = (
+ "fan.test_7",
+ "off",
+ {
+ "friendly_name": "Test fan 7",
+ "supported_features": 8,
+ "preset_modes": ["auto", "eco", "smart", "whoosh"],
+ "preset_mode": "auto",
+ },
+ )
+ appliance = await discovery_test(device, hass)
+
+ assert appliance["endpointId"] == "fan#test_7"
+ assert appliance["displayCategories"][0] == "FAN"
+ assert appliance["friendlyName"] == "Test fan 7"
+
+ capabilities = assert_endpoint_capabilities(
+ appliance,
+ "Alexa.EndpointHealth",
+ "Alexa.ModeController",
+ "Alexa.PowerController",
+ "Alexa",
+ )
+
+ range_capability = get_capability(capabilities, "Alexa.ModeController")
+ assert range_capability is not None
+ assert range_capability["instance"] == "fan.preset_mode"
+
+ properties = range_capability["properties"]
+ assert properties["nonControllable"] is False
+ assert {"name": "mode"} in properties["supported"]
+
+ capability_resources = range_capability["capabilityResources"]
+ assert capability_resources is not None
+ assert {
+ "@type": "asset",
+ "value": {"assetId": "Alexa.Setting.Preset"},
+ } in capability_resources["friendlyNames"]
+
+ configuration = range_capability["configuration"]
+ assert configuration is not None
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.ModeController",
+ "SetMode",
+ "fan#test_7",
+ "fan.set_preset_mode",
+ hass,
+ payload={"mode": "preset_mode.eco"},
+ instance="fan.preset_mode",
+ )
+ assert call.data["preset_mode"] == "eco"
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.ModeController",
+ "SetMode",
+ "fan#test_7",
+ "fan.set_preset_mode",
+ hass,
+ payload={"mode": "preset_mode.whoosh"},
+ instance="fan.preset_mode",
+ )
+ assert call.data["preset_mode"] == "whoosh"
+
+ with pytest.raises(AssertionError):
+ await assert_request_calls_service(
+ "Alexa.ModeController",
+ "SetMode",
+ "fan#test_7",
+ "fan.set_preset_mode",
+ hass,
+ payload={"mode": "preset_mode.invalid"},
+ instance="fan.preset_mode",
+ )
+ assert "Entity 'fan.test_7' does not support Preset 'invalid'" in caplog.text
+ caplog.clear()
+
+
async def test_lock(hass):
"""Test lock discovery."""
device = ("lock.test", "off", {"friendly_name": "Test lock"})
@@ -2484,7 +2567,7 @@ async def test_alarm_control_panel_disarmed(hass):
properties = ReportedProperties(msg["context"]["properties"])
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
- call, msg = await assert_request_calls_service(
+ _, msg = await assert_request_calls_service(
"Alexa.SecurityPanelController",
"Arm",
"alarm_control_panel#test_1",
diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py
index 2cbf8636d79..bbe80f29eef 100644
--- a/tests/components/alexa/test_state_report.py
+++ b/tests/components/alexa/test_state_report.py
@@ -50,10 +50,13 @@ async def test_report_state_instance(hass, aioclient_mock):
"off",
{
"friendly_name": "Test fan",
- "supported_features": 3,
- "speed": "off",
+ "supported_features": 15,
+ "speed": None,
"speed_list": ["off", "low", "high"],
"oscillating": False,
+ "preset_mode": None,
+ "preset_modes": ["auto", "smart"],
+ "percentage": None,
},
)
@@ -64,10 +67,13 @@ async def test_report_state_instance(hass, aioclient_mock):
"on",
{
"friendly_name": "Test fan",
- "supported_features": 3,
+ "supported_features": 15,
"speed": "high",
"speed_list": ["off", "low", "high"],
"oscillating": True,
+ "preset_mode": "smart",
+ "preset_modes": ["auto", "smart"],
+ "percentage": 90,
},
)
@@ -82,11 +88,33 @@ async def test_report_state_instance(hass, aioclient_mock):
assert call_json["event"]["header"]["name"] == "ChangeReport"
change_reports = call_json["event"]["payload"]["change"]["properties"]
+
+ checks = 0
for report in change_reports:
if report["name"] == "toggleState":
assert report["value"] == "ON"
assert report["instance"] == "fan.oscillating"
assert report["namespace"] == "Alexa.ToggleController"
+ checks += 1
+ if report["name"] == "mode":
+ assert report["value"] == "preset_mode.smart"
+ assert report["instance"] == "fan.preset_mode"
+ assert report["namespace"] == "Alexa.ModeController"
+ checks += 1
+ if report["name"] == "percentage":
+ assert report["value"] == 90
+ assert report["namespace"] == "Alexa.PercentageController"
+ checks += 1
+ if report["name"] == "powerLevel":
+ assert report["value"] == 90
+ assert report["namespace"] == "Alexa.PowerLevelController"
+ checks += 1
+ if report["name"] == "rangeValue":
+ assert report["value"] == 2
+ assert report["instance"] == "fan.speed"
+ assert report["namespace"] == "Alexa.RangeController"
+ checks += 1
+ assert checks == 5
assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan"
diff --git a/tests/components/ambee/__init__.py b/tests/components/ambee/__init__.py
new file mode 100644
index 00000000000..94c88557803
--- /dev/null
+++ b/tests/components/ambee/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Ambee integration."""
diff --git a/tests/components/ambee/conftest.py b/tests/components/ambee/conftest.py
new file mode 100644
index 00000000000..d6dd53a9711
--- /dev/null
+++ b/tests/components/ambee/conftest.py
@@ -0,0 +1,53 @@
+"""Fixtures for Ambee integration tests."""
+import json
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from ambee import AirQuality, Pollen
+import pytest
+
+from homeassistant.components.ambee.const import DOMAIN
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry, load_fixture
+from tests.test_util.aiohttp import AiohttpClientMocker
+
+
+@pytest.fixture
+def mock_config_entry() -> MockConfigEntry:
+ """Return the default mocked config entry."""
+ return MockConfigEntry(
+ title="Home Sweet Home",
+ domain=DOMAIN,
+ data={CONF_LATITUDE: 52.42, CONF_LONGITUDE: 4.44, CONF_API_KEY: "example"},
+ unique_id="unique_thingy",
+ )
+
+
+@pytest.fixture
+def mock_ambee(aioclient_mock: AiohttpClientMocker):
+ """Return a mocked Ambee client."""
+ with patch("homeassistant.components.ambee.Ambee") as ambee_mock:
+ client = ambee_mock.return_value
+ client.air_quality = AsyncMock(
+ return_value=AirQuality.from_dict(
+ json.loads(load_fixture("ambee/air_quality.json"))
+ )
+ )
+ client.pollen = AsyncMock(
+ return_value=Pollen.from_dict(json.loads(load_fixture("ambee/pollen.json")))
+ )
+ yield ambee_mock
+
+
+@pytest.fixture
+async def init_integration(
+ hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ambee: MagicMock
+) -> MockConfigEntry:
+ """Set up the Ambee integration for testing."""
+ mock_config_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ return mock_config_entry
diff --git a/tests/components/ambee/test_config_flow.py b/tests/components/ambee/test_config_flow.py
new file mode 100644
index 00000000000..a6220418681
--- /dev/null
+++ b/tests/components/ambee/test_config_flow.py
@@ -0,0 +1,272 @@
+"""Tests for the Ambee config flow."""
+
+from unittest.mock import patch
+
+from ambee import AmbeeAuthenticationError, AmbeeError
+
+from homeassistant.components.ambee.const import DOMAIN
+from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, 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) -> 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
+
+ with patch(
+ "homeassistant.components.ambee.config_flow.Ambee.air_quality"
+ ) as mock_ambee, patch(
+ "homeassistant.components.ambee.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_NAME: "Name",
+ CONF_API_KEY: "example",
+ CONF_LATITUDE: 52.42,
+ CONF_LONGITUDE: 4.44,
+ },
+ )
+
+ assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
+ assert result2.get("title") == "Name"
+ assert result2.get("data") == {
+ CONF_API_KEY: "example",
+ CONF_LATITUDE: 52.42,
+ CONF_LONGITUDE: 4.44,
+ }
+
+ assert len(mock_setup_entry.mock_calls) == 1
+ assert len(mock_ambee.mock_calls) == 1
+
+
+async def test_full_flow_with_authentication_error(hass: HomeAssistant) -> None:
+ """Test the full user configuration flow with an authentication error.
+
+ This tests tests a full config flow, with a case the user enters an invalid
+ API token, but recover by entering the correct one.
+ """
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+
+ assert result.get("type") == RESULT_TYPE_FORM
+ assert result.get("step_id") == SOURCE_USER
+ assert "flow_id" in result
+
+ with patch(
+ "homeassistant.components.ambee.config_flow.Ambee.air_quality",
+ side_effect=AmbeeAuthenticationError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_NAME: "Name",
+ CONF_API_KEY: "invalid",
+ CONF_LATITUDE: 52.42,
+ CONF_LONGITUDE: 4.44,
+ },
+ )
+
+ assert result2.get("type") == RESULT_TYPE_FORM
+ assert result2.get("step_id") == SOURCE_USER
+ assert result2.get("errors") == {"base": "invalid_api_key"}
+ assert "flow_id" in result2
+
+ with patch(
+ "homeassistant.components.ambee.config_flow.Ambee.air_quality"
+ ) as mock_ambee, patch(
+ "homeassistant.components.ambee.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ result3 = await hass.config_entries.flow.async_configure(
+ result2["flow_id"],
+ user_input={
+ CONF_NAME: "Name",
+ CONF_API_KEY: "example",
+ CONF_LATITUDE: 52.42,
+ CONF_LONGITUDE: 4.44,
+ },
+ )
+
+ assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY
+ assert result3.get("title") == "Name"
+ assert result3.get("data") == {
+ CONF_API_KEY: "example",
+ CONF_LATITUDE: 52.42,
+ CONF_LONGITUDE: 4.44,
+ }
+
+ assert len(mock_setup_entry.mock_calls) == 1
+ assert len(mock_ambee.mock_calls) == 1
+
+
+async def test_api_error(hass: HomeAssistant) -> None:
+ """Test API error."""
+ with patch(
+ "homeassistant.components.ambee.Ambee.air_quality",
+ side_effect=AmbeeError,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={
+ CONF_NAME: "Name",
+ CONF_API_KEY: "example",
+ CONF_LATITUDE: 52.42,
+ CONF_LONGITUDE: 4.44,
+ },
+ )
+
+ assert result.get("type") == RESULT_TYPE_FORM
+ assert result.get("errors") == {"base": "cannot_connect"}
+
+
+async def test_reauth_flow(
+ hass: HomeAssistant, mock_config_entry: MockConfigEntry
+) -> None:
+ """Test the reauthentication configuration flow."""
+ mock_config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": SOURCE_REAUTH,
+ "unique_id": mock_config_entry.unique_id,
+ "entry_id": mock_config_entry.entry_id,
+ },
+ data=mock_config_entry.data,
+ )
+ assert result.get("type") == RESULT_TYPE_FORM
+ assert result.get("step_id") == "reauth_confirm"
+ assert "flow_id" in result
+
+ with patch(
+ "homeassistant.components.ambee.config_flow.Ambee.air_quality"
+ ) as mock_ambee, patch(
+ "homeassistant.components.ambee.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_API_KEY: "other_key"},
+ )
+ await hass.async_block_till_done()
+
+ assert result2.get("type") == RESULT_TYPE_ABORT
+ assert result2.get("reason") == "reauth_successful"
+ assert mock_config_entry.data == {
+ CONF_API_KEY: "other_key",
+ CONF_LATITUDE: 52.42,
+ CONF_LONGITUDE: 4.44,
+ }
+
+ assert len(mock_ambee.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_reauth_with_authentication_error(
+ hass: HomeAssistant, mock_config_entry: MockConfigEntry
+) -> None:
+ """Test the reauthentication configuration flow with an authentication error.
+
+ This tests tests a reauth flow, with a case the user enters an invalid
+ API token, but recover by entering the correct one.
+ """
+ mock_config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": SOURCE_REAUTH,
+ "unique_id": mock_config_entry.unique_id,
+ "entry_id": mock_config_entry.entry_id,
+ },
+ data=mock_config_entry.data,
+ )
+ assert result.get("type") == RESULT_TYPE_FORM
+ assert result.get("step_id") == "reauth_confirm"
+ assert "flow_id" in result
+
+ with patch(
+ "homeassistant.components.ambee.config_flow.Ambee.air_quality",
+ side_effect=AmbeeAuthenticationError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_API_KEY: "invalid",
+ },
+ )
+
+ assert result2.get("type") == RESULT_TYPE_FORM
+ assert result2.get("step_id") == "reauth_confirm"
+ assert result2.get("errors") == {"base": "invalid_api_key"}
+ assert "flow_id" in result2
+
+ with patch(
+ "homeassistant.components.ambee.config_flow.Ambee.air_quality"
+ ) as mock_ambee, patch(
+ "homeassistant.components.ambee.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ result3 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_API_KEY: "other_key"},
+ )
+ await hass.async_block_till_done()
+
+ assert result3.get("type") == RESULT_TYPE_ABORT
+ assert result3.get("reason") == "reauth_successful"
+ assert mock_config_entry.data == {
+ CONF_API_KEY: "other_key",
+ CONF_LATITUDE: 52.42,
+ CONF_LONGITUDE: 4.44,
+ }
+
+ assert len(mock_ambee.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_reauth_api_error(
+ hass: HomeAssistant, mock_config_entry: MockConfigEntry
+) -> None:
+ """Test API error during reauthentication."""
+ mock_config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": SOURCE_REAUTH,
+ "unique_id": mock_config_entry.unique_id,
+ "entry_id": mock_config_entry.entry_id,
+ },
+ data=mock_config_entry.data,
+ )
+ assert "flow_id" in result
+
+ with patch(
+ "homeassistant.components.ambee.config_flow.Ambee.air_quality",
+ side_effect=AmbeeError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_API_KEY: "invalid",
+ },
+ )
+
+ assert result2.get("type") == RESULT_TYPE_FORM
+ assert result2.get("step_id") == "reauth_confirm"
+ assert result2.get("errors") == {"base": "cannot_connect"}
diff --git a/tests/components/ambee/test_init.py b/tests/components/ambee/test_init.py
new file mode 100644
index 00000000000..c6ad45735ff
--- /dev/null
+++ b/tests/components/ambee/test_init.py
@@ -0,0 +1,78 @@
+"""Tests for the Ambee integration."""
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from ambee import AmbeeConnectionError
+from ambee.exceptions import AmbeeAuthenticationError
+import pytest
+
+from homeassistant.components.ambee.const import DOMAIN
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry
+
+
+async def test_load_unload_config_entry(
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+ mock_ambee: AsyncMock,
+) -> None:
+ """Test the Ambee 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)
+
+
+@patch(
+ "homeassistant.components.ambee.Ambee.request",
+ side_effect=AmbeeConnectionError,
+)
+async def test_config_entry_not_ready(
+ mock_request: MagicMock,
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+) -> None:
+ """Test the Ambee configuration entry not ready."""
+ mock_config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert mock_request.call_count == 1
+ assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
+
+
+@pytest.mark.parametrize("service_name", ["air_quality", "pollen"])
+async def test_config_entry_authentication_failed(
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+ mock_ambee: MagicMock,
+ service_name: str,
+) -> None:
+ """Test the Ambee configuration entry not ready."""
+ mock_config_entry.add_to_hass(hass)
+
+ service = getattr(mock_ambee.return_value, service_name)
+ service.side_effect = AmbeeAuthenticationError
+
+ 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_ERROR
+
+ flows = hass.config_entries.flow.async_progress()
+ assert len(flows) == 1
+
+ flow = flows[0]
+ assert flow.get("step_id") == "reauth_confirm"
+ assert flow.get("handler") == DOMAIN
+
+ assert "context" in flow
+ assert flow["context"].get("source") == SOURCE_REAUTH
+ assert flow["context"].get("entry_id") == mock_config_entry.entry_id
diff --git a/tests/components/ambee/test_sensor.py b/tests/components/ambee/test_sensor.py
new file mode 100644
index 00000000000..34eaa273901
--- /dev/null
+++ b/tests/components/ambee/test_sensor.py
@@ -0,0 +1,349 @@
+"""Tests for the sensors provided by the Ambee integration."""
+from unittest.mock import AsyncMock
+
+import pytest
+
+from homeassistant.components.ambee.const import (
+ DEVICE_CLASS_AMBEE_RISK,
+ DOMAIN,
+ ENTRY_TYPE_SERVICE,
+)
+from homeassistant.components.sensor import (
+ ATTR_STATE_CLASS,
+ DOMAIN as SENSOR_DOMAIN,
+ STATE_CLASS_MEASUREMENT,
+)
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_FRIENDLY_NAME,
+ ATTR_ICON,
+ ATTR_UNIT_OF_MEASUREMENT,
+ CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ CONCENTRATION_PARTS_PER_BILLION,
+ CONCENTRATION_PARTS_PER_CUBIC_METER,
+ CONCENTRATION_PARTS_PER_MILLION,
+ DEVICE_CLASS_CO,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+
+from tests.common import MockConfigEntry
+
+
+async def test_air_quality(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+) -> None:
+ """Test the Ambee Air Quality sensors."""
+ entry_id = init_integration.entry_id
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
+
+ state = hass.states.get("sensor.air_quality_particulate_matter_2_5")
+ entry = entity_registry.async_get("sensor.air_quality_particulate_matter_2_5")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_2_5"
+ assert state.state == "3.14"
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 2.5 μm"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+ )
+ assert ATTR_DEVICE_CLASS not in state.attributes
+ assert ATTR_ICON not in state.attributes
+
+ state = hass.states.get("sensor.air_quality_particulate_matter_10")
+ entry = entity_registry.async_get("sensor.air_quality_particulate_matter_10")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_10"
+ assert state.state == "5.24"
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 10 μm"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+ )
+ assert ATTR_DEVICE_CLASS not in state.attributes
+ assert ATTR_ICON not in state.attributes
+
+ state = hass.states.get("sensor.air_quality_sulphur_dioxide")
+ entry = entity_registry.async_get("sensor.air_quality_sulphur_dioxide")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_air_quality_sulphur_dioxide"
+ assert state.state == "0.031"
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sulphur Dioxide (SO2)"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_PARTS_PER_BILLION
+ )
+ assert ATTR_DEVICE_CLASS not in state.attributes
+ assert ATTR_ICON not in state.attributes
+
+ state = hass.states.get("sensor.air_quality_nitrogen_dioxide")
+ entry = entity_registry.async_get("sensor.air_quality_nitrogen_dioxide")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_air_quality_nitrogen_dioxide"
+ assert state.state == "0.66"
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Nitrogen Dioxide (NO2)"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_PARTS_PER_BILLION
+ )
+ assert ATTR_DEVICE_CLASS not in state.attributes
+ assert ATTR_ICON not in state.attributes
+
+ state = hass.states.get("sensor.air_quality_ozone")
+ entry = entity_registry.async_get("sensor.air_quality_ozone")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_air_quality_ozone"
+ assert state.state == "17.067"
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ozone"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_PARTS_PER_BILLION
+ )
+ assert ATTR_DEVICE_CLASS not in state.attributes
+ assert ATTR_ICON not in state.attributes
+
+ state = hass.states.get("sensor.air_quality_carbon_monoxide")
+ entry = entity_registry.async_get("sensor.air_quality_carbon_monoxide")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_air_quality_carbon_monoxide"
+ assert state.state == "0.105"
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Carbon Monoxide (CO)"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_PARTS_PER_MILLION
+ )
+ assert ATTR_ICON not in state.attributes
+
+ state = hass.states.get("sensor.air_quality_air_quality_index")
+ entry = entity_registry.async_get("sensor.air_quality_air_quality_index")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_air_quality_air_quality_index"
+ assert state.state == "13"
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air Quality Index (AQI)"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert ATTR_DEVICE_CLASS not in state.attributes
+ assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
+ assert ATTR_ICON not in state.attributes
+
+ assert entry.device_id
+ device_entry = device_registry.async_get(entry.device_id)
+ assert device_entry
+ assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_air_quality")}
+ assert device_entry.manufacturer == "Ambee"
+ assert device_entry.name == "Air Quality"
+ assert device_entry.entry_type == ENTRY_TYPE_SERVICE
+ assert not device_entry.model
+ assert not device_entry.sw_version
+
+
+async def test_pollen(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+) -> None:
+ """Test the Ambee Pollen sensors."""
+ entry_id = init_integration.entry_id
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
+
+ state = hass.states.get("sensor.pollen_grass")
+ entry = entity_registry.async_get("sensor.pollen_grass")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_pollen_grass"
+ assert state.state == "190"
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen"
+ assert state.attributes.get(ATTR_ICON) == "mdi:grass"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_PARTS_PER_CUBIC_METER
+ )
+ assert ATTR_DEVICE_CLASS not in state.attributes
+
+ state = hass.states.get("sensor.pollen_tree")
+ entry = entity_registry.async_get("sensor.pollen_tree")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_pollen_tree"
+ assert state.state == "127"
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen"
+ assert state.attributes.get(ATTR_ICON) == "mdi:tree"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_PARTS_PER_CUBIC_METER
+ )
+ assert ATTR_DEVICE_CLASS not in state.attributes
+
+ state = hass.states.get("sensor.pollen_weed")
+ entry = entity_registry.async_get("sensor.pollen_weed")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_pollen_weed"
+ assert state.state == "95"
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen"
+ assert state.attributes.get(ATTR_ICON) == "mdi:sprout"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_PARTS_PER_CUBIC_METER
+ )
+ assert ATTR_DEVICE_CLASS not in state.attributes
+
+ state = hass.states.get("sensor.pollen_grass_risk")
+ entry = entity_registry.async_get("sensor.pollen_grass_risk")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_pollen_grass_risk"
+ assert state.state == "high"
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen Risk"
+ assert state.attributes.get(ATTR_ICON) == "mdi:grass"
+ assert ATTR_STATE_CLASS not in state.attributes
+ assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
+
+ state = hass.states.get("sensor.pollen_tree_risk")
+ entry = entity_registry.async_get("sensor.pollen_tree_risk")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_pollen_tree_risk"
+ assert state.state == "moderate"
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen Risk"
+ assert state.attributes.get(ATTR_ICON) == "mdi:tree"
+ assert ATTR_STATE_CLASS not in state.attributes
+ assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
+
+ state = hass.states.get("sensor.pollen_weed_risk")
+ entry = entity_registry.async_get("sensor.pollen_weed_risk")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_pollen_weed_risk"
+ assert state.state == "high"
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen Risk"
+ assert state.attributes.get(ATTR_ICON) == "mdi:sprout"
+ assert ATTR_STATE_CLASS not in state.attributes
+ assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
+
+ assert entry.device_id
+ device_entry = device_registry.async_get(entry.device_id)
+ assert device_entry
+ assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_pollen")}
+ assert device_entry.manufacturer == "Ambee"
+ assert device_entry.name == "Pollen"
+ assert device_entry.entry_type == ENTRY_TYPE_SERVICE
+ assert not device_entry.model
+ assert not device_entry.sw_version
+
+
+@pytest.mark.parametrize(
+ "entity_id",
+ (
+ "sensor.pollen_grass_poaceae",
+ "sensor.pollen_tree_alder",
+ "sensor.pollen_tree_birch",
+ "sensor.pollen_tree_cypress",
+ "sensor.pollen_tree_elm",
+ "sensor.pollen_tree_hazel",
+ "sensor.pollen_tree_oak",
+ "sensor.pollen_tree_pine",
+ "sensor.pollen_tree_plane",
+ "sensor.pollen_tree_poplar",
+ "sensor.pollen_weed_chenopod",
+ "sensor.pollen_weed_mugwort",
+ "sensor.pollen_weed_nettle",
+ "sensor.pollen_weed_ragweed",
+ ),
+)
+async def test_pollen_disabled_by_default(
+ hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str
+) -> None:
+ """Test the Ambee Pollen sensors that are disabled by default."""
+ entity_registry = er.async_get(hass)
+
+ state = hass.states.get(entity_id)
+ assert state is None
+
+ entry = entity_registry.async_get(entity_id)
+ assert entry
+ assert entry.disabled
+ assert entry.disabled_by == er.DISABLED_INTEGRATION
+
+
+@pytest.mark.parametrize(
+ "key,icon,name,value",
+ [
+ ("grass_poaceae", "mdi:grass", "Poaceae Grass Pollen", "190"),
+ ("tree_alder", "mdi:tree", "Alder Tree Pollen", "0"),
+ ("tree_birch", "mdi:tree", "Birch Tree Pollen", "35"),
+ ("tree_cypress", "mdi:tree", "Cypress Tree Pollen", "0"),
+ ("tree_elm", "mdi:tree", "Elm Tree Pollen", "0"),
+ ("tree_hazel", "mdi:tree", "Hazel Tree Pollen", "0"),
+ ("tree_oak", "mdi:tree", "Oak Tree Pollen", "55"),
+ ("tree_pine", "mdi:tree", "Pine Tree Pollen", "30"),
+ ("tree_plane", "mdi:tree", "Plane Tree Pollen", "5"),
+ ("tree_poplar", "mdi:tree", "Poplar Tree Pollen", "0"),
+ ("weed_chenopod", "mdi:sprout", "Chenopod Weed Pollen", "0"),
+ ("weed_mugwort", "mdi:sprout", "Mugwort Weed Pollen", "1"),
+ ("weed_nettle", "mdi:sprout", "Nettle Weed Pollen", "88"),
+ ("weed_ragweed", "mdi:sprout", "Ragweed Weed Pollen", "3"),
+ ],
+)
+async def test_pollen_enable_disable_by_defaults(
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+ mock_ambee: AsyncMock,
+ key: str,
+ icon: str,
+ name: str,
+ value: str,
+) -> None:
+ """Test the Ambee Pollen sensors that are disabled by default."""
+ entry_id = mock_config_entry.entry_id
+ entity_id = f"{SENSOR_DOMAIN}.pollen_{key}"
+ entity_registry = er.async_get(hass)
+
+ # Pre-create registry entry for disabled by default sensor
+ entity_registry.async_get_or_create(
+ SENSOR_DOMAIN,
+ DOMAIN,
+ f"{entry_id}_pollen_{key}",
+ suggested_object_id=f"pollen_{key}",
+ disabled_by=None,
+ )
+
+ mock_config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity_id)
+ entry = entity_registry.async_get(entity_id)
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_pollen_{key}"
+ assert state.state == value
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == name
+ assert state.attributes.get(ATTR_ICON) == icon
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_PARTS_PER_CUBIC_METER
+ )
+ assert ATTR_DEVICE_CLASS not in state.attributes
diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py
index 87c3fadb978..19c27777c2a 100644
--- a/tests/components/asuswrt/test_sensor.py
+++ b/tests/components/asuswrt/test_sensor.py
@@ -20,6 +20,7 @@ from homeassistant.const import (
STATE_NOT_HOME,
)
from homeassistant.helpers import entity_registry as er
+from homeassistant.util import slugify
from homeassistant.util.dt import utcnow
from tests.common import MockConfigEntry, async_fire_time_changed
@@ -38,6 +39,18 @@ CONFIG_DATA = {
MOCK_BYTES_TOTAL = [60000000000, 50000000000]
MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000]
+MOCK_LOAD_AVG = [1.1, 1.2, 1.3]
+
+SENSOR_NAMES = [
+ "Devices Connected",
+ "Download Speed",
+ "Download",
+ "Upload Speed",
+ "Upload",
+ "Load Avg (1m)",
+ "Load Avg (5m)",
+ "Load Avg (15m)",
+]
@pytest.fixture(name="mock_devices")
@@ -72,6 +85,9 @@ def mock_controller_connect(mock_devices):
service_mock.return_value.async_get_current_transfer_rates = AsyncMock(
return_value=MOCK_CURRENT_TRANSFER_RATES
)
+ service_mock.return_value.async_get_loadavg = AsyncMock(
+ return_value=MOCK_LOAD_AVG
+ )
yield service_mock
@@ -88,46 +104,19 @@ async def test_sensors(hass, connect, mock_devices):
# init variable
unique_id = DOMAIN
- name_prefix = DEFAULT_PREFIX
- obj_prefix = name_prefix.lower()
+ obj_prefix = slugify(DEFAULT_PREFIX)
sensor_prefix = f"{sensor.DOMAIN}.{obj_prefix}"
# Pre-enable the status sensor
- entity_reg.async_get_or_create(
- sensor.DOMAIN,
- DOMAIN,
- f"{unique_id} {name_prefix} Devices Connected",
- suggested_object_id=f"{obj_prefix}_devices_connected",
- disabled_by=None,
- )
- entity_reg.async_get_or_create(
- sensor.DOMAIN,
- DOMAIN,
- f"{unique_id} {name_prefix} Download Speed",
- suggested_object_id=f"{obj_prefix}_download_speed",
- disabled_by=None,
- )
- entity_reg.async_get_or_create(
- sensor.DOMAIN,
- DOMAIN,
- f"{unique_id} {name_prefix} Download",
- suggested_object_id=f"{obj_prefix}_download",
- disabled_by=None,
- )
- entity_reg.async_get_or_create(
- sensor.DOMAIN,
- DOMAIN,
- f"{unique_id} {name_prefix} Upload Speed",
- suggested_object_id=f"{obj_prefix}_upload_speed",
- disabled_by=None,
- )
- entity_reg.async_get_or_create(
- sensor.DOMAIN,
- DOMAIN,
- f"{unique_id} {name_prefix} Upload",
- suggested_object_id=f"{obj_prefix}_upload",
- disabled_by=None,
- )
+ for sensor_name in SENSOR_NAMES:
+ sensor_id = slugify(sensor_name)
+ entity_reg.async_get_or_create(
+ sensor.DOMAIN,
+ DOMAIN,
+ f"{unique_id} {DEFAULT_PREFIX} {sensor_name}",
+ suggested_object_id=f"{obj_prefix}_{sensor_id}",
+ disabled_by=None,
+ )
config_entry.add_to_hass(hass)
@@ -143,6 +132,9 @@ async def test_sensors(hass, connect, mock_devices):
assert hass.states.get(f"{sensor_prefix}_download").state == "60.0"
assert hass.states.get(f"{sensor_prefix}_upload_speed").state == "80.0"
assert hass.states.get(f"{sensor_prefix}_upload").state == "50.0"
+ assert hass.states.get(f"{sensor_prefix}_load_avg_1m").state == "1.1"
+ assert hass.states.get(f"{sensor_prefix}_load_avg_5m").state == "1.2"
+ assert hass.states.get(f"{sensor_prefix}_load_avg_15m").state == "1.3"
assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "2"
# add one device and remove another
diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py
index 5997be22644..80fe5c52abc 100644
--- a/tests/components/automation/test_init.py
+++ b/tests/components/automation/test_init.py
@@ -1405,3 +1405,97 @@ async def test_trigger_service(hass, calls):
assert len(calls) == 1
assert calls[0].data.get("trigger") == {"platform": None}
assert calls[0].context.parent_id is context.id
+
+
+async def test_trigger_condition_implicit_id(hass, calls):
+ """Test triggers."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": [
+ {"platform": "event", "event_type": "test_event1"},
+ {"platform": "event", "event_type": "test_event2"},
+ {"platform": "event", "event_type": "test_event3"},
+ ],
+ "action": {
+ "choose": [
+ {
+ "conditions": {"condition": "trigger", "id": [0, "2"]},
+ "sequence": {
+ "service": "test.automation",
+ "data": {"param": "one"},
+ },
+ },
+ {
+ "conditions": {"condition": "trigger", "id": "1"},
+ "sequence": {
+ "service": "test.automation",
+ "data": {"param": "two"},
+ },
+ },
+ ]
+ },
+ }
+ },
+ )
+
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[-1].data.get("param") == "one"
+
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[-1].data.get("param") == "two"
+
+ hass.bus.async_fire("test_event3")
+ await hass.async_block_till_done()
+ assert len(calls) == 3
+ assert calls[-1].data.get("param") == "one"
+
+
+async def test_trigger_condition_explicit_id(hass, calls):
+ """Test triggers."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": [
+ {"platform": "event", "event_type": "test_event1", "id": "one"},
+ {"platform": "event", "event_type": "test_event2", "id": "two"},
+ ],
+ "action": {
+ "choose": [
+ {
+ "conditions": {"condition": "trigger", "id": "one"},
+ "sequence": {
+ "service": "test.automation",
+ "data": {"param": "one"},
+ },
+ },
+ {
+ "conditions": {"condition": "trigger", "id": "two"},
+ "sequence": {
+ "service": "test.automation",
+ "data": {"param": "two"},
+ },
+ },
+ ]
+ },
+ }
+ },
+ )
+
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[-1].data.get("param") == "one"
+
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[-1].data.get("param") == "two"
diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py
index 5d8673825fc..3d1b694c7ce 100644
--- a/tests/components/binary_sensor/test_device_condition.py
+++ b/tests/components/binary_sensor/test_device_condition.py
@@ -78,6 +78,41 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr
assert conditions == expected_conditions
+async def test_get_conditions_no_state(hass, device_reg, entity_reg):
+ """Test we get the expected conditions from a binary_sensor."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_ids = {}
+ for device_class in DEVICE_CLASSES:
+ entity_ids[device_class] = entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ f"5678_{device_class}",
+ device_id=device_entry.id,
+ device_class=device_class,
+ ).entity_id
+
+ await hass.async_block_till_done()
+
+ expected_conditions = [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": condition["type"],
+ "device_id": device_entry.id,
+ "entity_id": entity_ids[device_class],
+ }
+ for device_class in DEVICE_CLASSES
+ for condition in ENTITY_CONDITIONS[device_class]
+ ]
+ conditions = await async_get_device_automations(hass, "condition", device_entry.id)
+ assert conditions == expected_conditions
+
+
async def test_get_condition_capabilities(hass, device_reg, entity_reg):
"""Test we get the expected capabilities from a binary_sensor condition."""
config_entry = MockConfigEntry(domain="test", data={})
diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py
index 0e5cbcc1d70..8bd80be6524 100644
--- a/tests/components/binary_sensor/test_device_trigger.py
+++ b/tests/components/binary_sensor/test_device_trigger.py
@@ -78,6 +78,44 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat
assert triggers == expected_triggers
+async def test_get_triggers_no_state(hass, device_reg, entity_reg):
+ """Test we get the expected triggers from a binary_sensor."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+ entity_ids = {}
+
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ for device_class in DEVICE_CLASSES:
+ entity_ids[device_class] = entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ f"5678_{device_class}",
+ device_id=device_entry.id,
+ device_class=device_class,
+ ).entity_id
+
+ await hass.async_block_till_done()
+
+ expected_triggers = [
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": trigger["type"],
+ "device_id": device_entry.id,
+ "entity_id": entity_ids[device_class],
+ }
+ for device_class in DEVICE_CLASSES
+ for trigger in ENTITY_TRIGGERS[device_class]
+ ]
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert triggers == expected_triggers
+
+
async def test_get_trigger_capabilities(hass, device_reg, entity_reg):
"""Test we get the expected capabilities from a binary_sensor trigger."""
config_entry = MockConfigEntry(domain="test", data={})
diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py
index 36c7ae9955a..7ac9439e711 100644
--- a/tests/components/braviatv/test_config_flow.py
+++ b/tests/components/braviatv/test_config_flow.py
@@ -5,7 +5,7 @@ from bravia_tv.braviarc import NoIPControl
from homeassistant import data_entry_flow
from homeassistant.components.braviatv.const import CONF_IGNORED_SOURCES, DOMAIN
-from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
+from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN
from tests.common import MockConfigEntry
@@ -31,9 +31,6 @@ BRAVIA_SOURCE_LIST = {
"AV/Component": "extInput:component?port=1",
}
-IMPORT_CONFIG_HOSTNAME = {CONF_HOST: "bravia-host", CONF_PIN: "1234"}
-IMPORT_CONFIG_IP = {CONF_HOST: "10.10.10.12", CONF_PIN: "1234"}
-
async def test_show_form(hass):
"""Test that the form is served with no input."""
@@ -45,92 +42,6 @@ async def test_show_form(hass):
assert result["step_id"] == SOURCE_USER
-async def test_import(hass):
- """Test that the import works."""
- with patch("bravia_tv.BraviaRC.connect", return_value=True), patch(
- "bravia_tv.BraviaRC.is_connected", return_value=True
- ), patch(
- "bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO
- ), patch(
- "homeassistant.components.braviatv.async_setup_entry", return_value=True
- ):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_HOSTNAME
- )
-
- assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["result"].unique_id == "very_unique_string"
- assert result["title"] == "TV-Model"
- assert result["data"] == {
- CONF_HOST: "bravia-host",
- CONF_PIN: "1234",
- CONF_MAC: "AA:BB:CC:DD:EE:FF",
- }
-
-
-async def test_import_cannot_connect(hass):
- """Test that errors are shown when cannot connect to the host during import."""
- with patch("bravia_tv.BraviaRC.connect", return_value=True), patch(
- "bravia_tv.BraviaRC.is_connected", return_value=False
- ):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_HOSTNAME
- )
-
- assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "cannot_connect"
-
-
-async def test_import_model_unsupported(hass):
- """Test that errors are shown when the TV is not supported during import."""
- with patch("bravia_tv.BraviaRC.connect", return_value=True), patch(
- "bravia_tv.BraviaRC.is_connected", return_value=True
- ), patch("bravia_tv.BraviaRC.get_system_info", return_value={}):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_IP
- )
-
- assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "unsupported_model"
-
-
-async def test_import_no_ip_control(hass):
- """Test that errors are shown when IP Control is disabled on the TV during import."""
- with patch("bravia_tv.BraviaRC.connect", side_effect=NoIPControl("No IP Control")):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_IP
- )
-
- assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "no_ip_control"
-
-
-async def test_import_duplicate_error(hass):
- """Test that errors are shown when duplicates are added during import."""
- config_entry = MockConfigEntry(
- domain=DOMAIN,
- unique_id="very_unique_string",
- data={
- CONF_HOST: "bravia-host",
- CONF_PIN: "1234",
- CONF_MAC: "AA:BB:CC:DD:EE:FF",
- },
- title="TV-Model",
- )
- config_entry.add_to_hass(hass)
-
- with patch("bravia_tv.BraviaRC.connect", return_value=True), patch(
- "bravia_tv.BraviaRC.is_connected", return_value=True
- ), patch("bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO):
-
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_HOSTNAME
- )
-
- assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_configured"
-
-
async def test_user_invalid_host(hass):
"""Test that errors are shown when the host is invalid."""
result = await hass.config_entries.flow.async_init(
diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py
index 780887551f2..c65870add96 100644
--- a/tests/components/broadlink/__init__.py
+++ b/tests/components/broadlink/__init__.py
@@ -1,4 +1,5 @@
"""Tests for the Broadlink integration."""
+from dataclasses import dataclass
from unittest.mock import MagicMock, patch
from homeassistant.components.broadlink.const import DOMAIN
@@ -70,6 +71,15 @@ BROADLINK_DEVICES = {
}
+@dataclass
+class MockSetup:
+ """Representation of a mock setup."""
+
+ api: MagicMock
+ entry: MockConfigEntry
+ factory: MagicMock
+
+
class BroadlinkDevice:
"""Representation of a Broadlink device."""
@@ -96,11 +106,11 @@ class BroadlinkDevice:
with patch(
"homeassistant.components.broadlink.device.blk.gendevice",
return_value=mock_api,
- ):
+ ) as mock_factory:
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
- return mock_api, mock_entry
+ return MockSetup(mock_api, mock_entry, mock_factory)
def get_mock_api(self):
"""Return a mock device (API)."""
diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py
index 2ee8f7a5218..5430af9e311 100644
--- a/tests/components/broadlink/test_device.py
+++ b/tests/components/broadlink/test_device.py
@@ -12,6 +12,8 @@ from . import get_device
from tests.common import mock_device_registry, mock_registry
+DEVICE_FACTORY = "homeassistant.components.broadlink.device.blk.gendevice"
+
async def test_device_setup(hass):
"""Test a successful setup."""
@@ -22,13 +24,15 @@ async def test_device_setup(hass):
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- mock_api, mock_entry = await device.setup_entry(hass)
+ mock_setup = await device.setup_entry(hass)
+
+ assert mock_setup.entry.state is ConfigEntryState.LOADED
+ assert mock_setup.api.auth.call_count == 1
+ assert mock_setup.api.get_fwversion.call_count == 1
+ assert mock_setup.factory.call_count == 1
- assert mock_entry.state == ConfigEntryState.LOADED
- assert mock_api.auth.call_count == 1
- assert mock_api.get_fwversion.call_count == 1
forward_entries = {c[1][1] for c in mock_forward.mock_calls}
- domains = get_domains(mock_api.type)
+ domains = get_domains(mock_setup.api.type)
assert mock_forward.call_count == len(domains)
assert forward_entries == domains
assert mock_init.call_count == 0
@@ -45,10 +49,10 @@ async def test_device_setup_authentication_error(hass):
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- assert mock_entry.state == ConfigEntryState.SETUP_ERROR
- assert mock_api.auth.call_count == 1
+ assert mock_setup.entry.state is ConfigEntryState.SETUP_ERROR
+ assert mock_setup.api.auth.call_count == 1
assert mock_forward.call_count == 0
assert mock_init.call_count == 1
assert mock_init.mock_calls[0][2]["context"]["source"] == "reauth"
@@ -69,10 +73,10 @@ async def test_device_setup_network_timeout(hass):
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- assert mock_entry.state is ConfigEntryState.SETUP_RETRY
- assert mock_api.auth.call_count == 1
+ assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY
+ assert mock_setup.api.auth.call_count == 1
assert mock_forward.call_count == 0
assert mock_init.call_count == 0
@@ -88,10 +92,10 @@ async def test_device_setup_os_error(hass):
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- assert mock_entry.state is ConfigEntryState.SETUP_RETRY
- assert mock_api.auth.call_count == 1
+ assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY
+ assert mock_setup.api.auth.call_count == 1
assert mock_forward.call_count == 0
assert mock_init.call_count == 0
@@ -107,10 +111,10 @@ async def test_device_setup_broadlink_exception(hass):
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- assert mock_entry.state is ConfigEntryState.SETUP_ERROR
- assert mock_api.auth.call_count == 1
+ assert mock_setup.entry.state is ConfigEntryState.SETUP_ERROR
+ assert mock_setup.api.auth.call_count == 1
assert mock_forward.call_count == 0
assert mock_init.call_count == 0
@@ -126,11 +130,11 @@ async def test_device_setup_update_network_timeout(hass):
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- assert mock_entry.state is ConfigEntryState.SETUP_RETRY
- assert mock_api.auth.call_count == 1
- assert mock_api.check_sensors.call_count == 1
+ assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY
+ assert mock_setup.api.auth.call_count == 1
+ assert mock_setup.api.check_sensors.call_count == 1
assert mock_forward.call_count == 0
assert mock_init.call_count == 0
@@ -149,11 +153,12 @@ async def test_device_setup_update_authorization_error(hass):
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
+
+ assert mock_setup.entry.state is ConfigEntryState.LOADED
+ assert mock_setup.api.auth.call_count == 2
+ assert mock_setup.api.check_sensors.call_count == 2
- assert mock_entry.state is ConfigEntryState.LOADED
- assert mock_api.auth.call_count == 2
- assert mock_api.check_sensors.call_count == 2
forward_entries = {c[1][1] for c in mock_forward.mock_calls}
domains = get_domains(mock_api.type)
assert mock_forward.call_count == len(domains)
@@ -173,11 +178,11 @@ async def test_device_setup_update_authentication_error(hass):
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- assert mock_entry.state is ConfigEntryState.SETUP_RETRY
- assert mock_api.auth.call_count == 2
- assert mock_api.check_sensors.call_count == 1
+ assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY
+ assert mock_setup.api.auth.call_count == 2
+ assert mock_setup.api.check_sensors.call_count == 1
assert mock_forward.call_count == 0
assert mock_init.call_count == 1
assert mock_init.mock_calls[0][2]["context"]["source"] == "reauth"
@@ -198,11 +203,11 @@ async def test_device_setup_update_broadlink_exception(hass):
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- assert mock_entry.state is ConfigEntryState.SETUP_RETRY
- assert mock_api.auth.call_count == 1
- assert mock_api.check_sensors.call_count == 1
+ assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY
+ assert mock_setup.api.auth.call_count == 1
+ assert mock_setup.api.check_sensors.call_count == 1
assert mock_forward.call_count == 0
assert mock_init.call_count == 0
@@ -214,11 +219,11 @@ async def test_device_setup_get_fwversion_broadlink_exception(hass):
mock_api.get_fwversion.side_effect = blke.BroadlinkException()
with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward:
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- assert mock_entry.state is ConfigEntryState.LOADED
+ assert mock_setup.entry.state is ConfigEntryState.LOADED
forward_entries = {c[1][1] for c in mock_forward.mock_calls}
- domains = get_domains(mock_api.type)
+ domains = get_domains(mock_setup.api.type)
assert mock_forward.call_count == len(domains)
assert forward_entries == domains
@@ -230,11 +235,11 @@ async def test_device_setup_get_fwversion_os_error(hass):
mock_api.get_fwversion.side_effect = OSError()
with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward:
- _, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- assert mock_entry.state is ConfigEntryState.LOADED
+ assert mock_setup.entry.state is ConfigEntryState.LOADED
forward_entries = {c[1][1] for c in mock_forward.mock_calls}
- domains = get_domains(mock_api.type)
+ domains = get_domains(mock_setup.api.type)
assert mock_forward.call_count == len(domains)
assert forward_entries == domains
@@ -246,12 +251,14 @@ async def test_device_setup_registry(hass):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- _, mock_entry = await device.setup_entry(hass)
+ mock_setup = await device.setup_entry(hass)
await hass.async_block_till_done()
assert len(device_registry.devices) == 1
- device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)})
+ device_entry = device_registry.async_get_device(
+ {(DOMAIN, mock_setup.entry.unique_id)}
+ )
assert device_entry.identifiers == {(DOMAIN, device.mac)}
assert device_entry.name == device.name
assert device_entry.model == device.model
@@ -267,16 +274,16 @@ async def test_device_unload_works(hass):
device = get_device("Office")
with patch.object(hass.config_entries, "async_forward_entry_setup"):
- mock_api, mock_entry = await device.setup_entry(hass)
+ mock_setup = await device.setup_entry(hass)
with patch.object(
hass.config_entries, "async_forward_entry_unload", return_value=True
) as mock_forward:
- await hass.config_entries.async_unload(mock_entry.entry_id)
+ await hass.config_entries.async_unload(mock_setup.entry.entry_id)
- assert mock_entry.state is ConfigEntryState.NOT_LOADED
+ assert mock_setup.entry.state is ConfigEntryState.NOT_LOADED
forward_entries = {c[1][1] for c in mock_forward.mock_calls}
- domains = get_domains(mock_api.type)
+ domains = get_domains(mock_setup.api.type)
assert mock_forward.call_count == len(domains)
assert forward_entries == domains
@@ -290,14 +297,14 @@ async def test_device_unload_authentication_error(hass):
with patch.object(hass.config_entries, "async_forward_entry_setup"), patch.object(
hass.config_entries.flow, "async_init"
):
- _, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
with patch.object(
hass.config_entries, "async_forward_entry_unload", return_value=True
) as mock_forward:
- await hass.config_entries.async_unload(mock_entry.entry_id)
+ await hass.config_entries.async_unload(mock_setup.entry.entry_id)
- assert mock_entry.state is ConfigEntryState.NOT_LOADED
+ assert mock_setup.entry.state is ConfigEntryState.NOT_LOADED
assert mock_forward.call_count == 0
@@ -308,14 +315,14 @@ async def test_device_unload_update_failed(hass):
mock_api.check_sensors.side_effect = blke.NetworkTimeoutError()
with patch.object(hass.config_entries, "async_forward_entry_setup"):
- _, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
with patch.object(
hass.config_entries, "async_forward_entry_unload", return_value=True
) as mock_forward:
- await hass.config_entries.async_unload(mock_entry.entry_id)
+ await hass.config_entries.async_unload(mock_setup.entry.entry_id)
- assert mock_entry.state is ConfigEntryState.NOT_LOADED
+ assert mock_setup.entry.state is ConfigEntryState.NOT_LOADED
assert mock_forward.call_count == 0
@@ -326,16 +333,16 @@ async def test_device_update_listener(hass):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass)
+ mock_setup = await device.setup_entry(hass)
await hass.async_block_till_done()
- with patch(
- "homeassistant.components.broadlink.device.blk.gendevice", return_value=mock_api
- ):
- hass.config_entries.async_update_entry(mock_entry, title="New Name")
+ with patch(DEVICE_FACTORY, return_value=mock_setup.api):
+ hass.config_entries.async_update_entry(mock_setup.entry, title="New Name")
await hass.async_block_till_done()
- device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)})
+ device_entry = device_registry.async_get_device(
+ {(DOMAIN, mock_setup.entry.unique_id)}
+ )
assert device_entry.name == "New Name"
for entry in async_entries_for_device(entity_registry, device_entry.id):
assert entry.original_name.startswith("New Name")
diff --git a/tests/components/broadlink/test_heartbeat.py b/tests/components/broadlink/test_heartbeat.py
index 8e52a562425..de47a16c0b9 100644
--- a/tests/components/broadlink/test_heartbeat.py
+++ b/tests/components/broadlink/test_heartbeat.py
@@ -72,10 +72,10 @@ async def test_heartbeat_unload(hass):
"""Test that the heartbeat is deactivated when the last config entry is removed."""
device = get_device("Office")
- _, mock_entry = await device.setup_entry(hass)
+ mock_setup = await device.setup_entry(hass)
await hass.async_block_till_done()
- await hass.config_entries.async_remove(mock_entry.entry_id)
+ await hass.config_entries.async_remove(mock_setup.entry.entry_id)
await hass.async_block_till_done()
with patch(DEVICE_PING) as mock_ping:
@@ -91,11 +91,11 @@ async def test_heartbeat_do_not_unload(hass):
device_a = get_device("Office")
device_b = get_device("Bedroom")
- _, mock_entry_a = await device_a.setup_entry(hass)
+ mock_setup = await device_a.setup_entry(hass)
await device_b.setup_entry(hass)
await hass.async_block_till_done()
- await hass.config_entries.async_remove(mock_entry_a.entry_id)
+ await hass.config_entries.async_remove(mock_setup.entry.entry_id)
await hass.async_block_till_done()
with patch(DEVICE_PING) as mock_ping:
diff --git a/tests/components/broadlink/test_remote.py b/tests/components/broadlink/test_remote.py
index 2d21b588c33..abc500479ea 100644
--- a/tests/components/broadlink/test_remote.py
+++ b/tests/components/broadlink/test_remote.py
@@ -28,10 +28,10 @@ async def test_remote_setup_works(hass):
for device in map(get_device, REMOTE_DEVICES):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass)
+ mock_setup = await device.setup_entry(hass)
device_entry = device_registry.async_get_device(
- {(DOMAIN, mock_entry.unique_id)}
+ {(DOMAIN, mock_setup.entry.unique_id)}
)
entries = async_entries_for_device(entity_registry, device_entry.id)
remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN}
@@ -40,7 +40,7 @@ async def test_remote_setup_works(hass):
remote = remotes.pop()
assert remote.original_name == f"{device.name} Remote"
assert hass.states.get(remote.entity_id).state == STATE_ON
- assert mock_api.auth.call_count == 1
+ assert mock_setup.api.auth.call_count == 1
async def test_remote_send_command(hass):
@@ -48,10 +48,10 @@ async def test_remote_send_command(hass):
for device in map(get_device, REMOTE_DEVICES):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass)
+ mock_setup = await device.setup_entry(hass)
device_entry = device_registry.async_get_device(
- {(DOMAIN, mock_entry.unique_id)}
+ {(DOMAIN, mock_setup.entry.unique_id)}
)
entries = async_entries_for_device(entity_registry, device_entry.id)
remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN}
@@ -65,9 +65,9 @@ async def test_remote_send_command(hass):
blocking=True,
)
- assert mock_api.send_data.call_count == 1
- assert mock_api.send_data.call_args == call(b64decode(IR_PACKET))
- assert mock_api.auth.call_count == 1
+ assert mock_setup.api.send_data.call_count == 1
+ assert mock_setup.api.send_data.call_args == call(b64decode(IR_PACKET))
+ assert mock_setup.api.auth.call_count == 1
async def test_remote_turn_off_turn_on(hass):
@@ -75,10 +75,10 @@ async def test_remote_turn_off_turn_on(hass):
for device in map(get_device, REMOTE_DEVICES):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass)
+ mock_setup = await device.setup_entry(hass)
device_entry = device_registry.async_get_device(
- {(DOMAIN, mock_entry.unique_id)}
+ {(DOMAIN, mock_setup.entry.unique_id)}
)
entries = async_entries_for_device(entity_registry, device_entry.id)
remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN}
@@ -99,7 +99,7 @@ async def test_remote_turn_off_turn_on(hass):
{"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET},
blocking=True,
)
- assert mock_api.send_data.call_count == 0
+ assert mock_setup.api.send_data.call_count == 0
await hass.services.async_call(
REMOTE_DOMAIN,
@@ -115,6 +115,6 @@ async def test_remote_turn_off_turn_on(hass):
{"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET},
blocking=True,
)
- assert mock_api.send_data.call_count == 1
- assert mock_api.send_data.call_args == call(b64decode(IR_PACKET))
- assert mock_api.auth.call_count == 1
+ assert mock_setup.api.send_data.call_count == 1
+ assert mock_setup.api.send_data.call_args == call(b64decode(IR_PACKET))
+ assert mock_setup.api.auth.call_count == 1
diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py
index 5cc75c28a73..1f8f913cfe4 100644
--- a/tests/components/broadlink/test_sensors.py
+++ b/tests/components/broadlink/test_sensors.py
@@ -22,10 +22,12 @@ async def test_a1_sensor_setup(hass):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
assert mock_api.check_sensors_raw.call_count == 1
- device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)})
+ device_entry = device_registry.async_get_device(
+ {(DOMAIN, mock_setup.entry.unique_id)}
+ )
entries = async_entries_for_device(entity_registry, device_entry.id)
sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN]
assert len(sensors) == 5
@@ -58,14 +60,16 @@ async def test_a1_sensor_update(hass):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)})
+ device_entry = device_registry.async_get_device(
+ {(DOMAIN, mock_setup.entry.unique_id)}
+ )
entries = async_entries_for_device(entity_registry, device_entry.id)
sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN]
assert len(sensors) == 5
- mock_api.check_sensors_raw.return_value = {
+ mock_setup.api.check_sensors_raw.return_value = {
"temperature": 22.5,
"humidity": 47.4,
"air_quality": 2,
@@ -75,7 +79,7 @@ async def test_a1_sensor_update(hass):
await hass.helpers.entity_component.async_update_entity(
next(iter(sensors)).entity_id
)
- assert mock_api.check_sensors_raw.call_count == 2
+ assert mock_setup.api.check_sensors_raw.call_count == 2
sensors_and_states = {
(sensor.original_name, hass.states.get(sensor.entity_id).state)
@@ -99,10 +103,12 @@ async def test_rm_pro_sensor_setup(hass):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
assert mock_api.check_sensors.call_count == 1
- device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)})
+ device_entry = device_registry.async_get_device(
+ {(DOMAIN, mock_setup.entry.unique_id)}
+ )
entries = async_entries_for_device(entity_registry, device_entry.id)
sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN]
assert len(sensors) == 1
@@ -123,18 +129,20 @@ async def test_rm_pro_sensor_update(hass):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)})
+ device_entry = device_registry.async_get_device(
+ {(DOMAIN, mock_setup.entry.unique_id)}
+ )
entries = async_entries_for_device(entity_registry, device_entry.id)
sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN]
assert len(sensors) == 1
- mock_api.check_sensors.return_value = {"temperature": 25.8}
+ mock_setup.api.check_sensors.return_value = {"temperature": 25.8}
await hass.helpers.entity_component.async_update_entity(
next(iter(sensors)).entity_id
)
- assert mock_api.check_sensors.call_count == 2
+ assert mock_setup.api.check_sensors.call_count == 2
sensors_and_states = {
(sensor.original_name, hass.states.get(sensor.entity_id).state)
@@ -155,18 +163,20 @@ async def test_rm_pro_filter_crazy_temperature(hass):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)})
+ device_entry = device_registry.async_get_device(
+ {(DOMAIN, mock_setup.entry.unique_id)}
+ )
entries = async_entries_for_device(entity_registry, device_entry.id)
sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN]
assert len(sensors) == 1
- mock_api.check_sensors.return_value = {"temperature": -7}
+ mock_setup.api.check_sensors.return_value = {"temperature": -7}
await hass.helpers.entity_component.async_update_entity(
next(iter(sensors)).entity_id
)
- assert mock_api.check_sensors.call_count == 2
+ assert mock_setup.api.check_sensors.call_count == 2
sensors_and_states = {
(sensor.original_name, hass.states.get(sensor.entity_id).state)
@@ -184,10 +194,12 @@ async def test_rm_mini3_no_sensor(hass):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
assert mock_api.check_sensors.call_count <= 1
- device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)})
+ device_entry = device_registry.async_get_device(
+ {(DOMAIN, mock_setup.entry.unique_id)}
+ )
entries = async_entries_for_device(entity_registry, device_entry.id)
sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN]
assert len(sensors) == 0
@@ -202,10 +214,12 @@ async def test_rm4_pro_hts2_sensor_setup(hass):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
assert mock_api.check_sensors.call_count == 1
- device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)})
+ device_entry = device_registry.async_get_device(
+ {(DOMAIN, mock_setup.entry.unique_id)}
+ )
entries = async_entries_for_device(entity_registry, device_entry.id)
sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN]
assert len(sensors) == 2
@@ -229,18 +243,20 @@ async def test_rm4_pro_hts2_sensor_update(hass):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
- device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)})
+ device_entry = device_registry.async_get_device(
+ {(DOMAIN, mock_setup.entry.unique_id)}
+ )
entries = async_entries_for_device(entity_registry, device_entry.id)
sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN]
assert len(sensors) == 2
- mock_api.check_sensors.return_value = {"temperature": 16.8, "humidity": 34.0}
+ 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
)
- assert mock_api.check_sensors.call_count == 2
+ assert mock_setup.api.check_sensors.call_count == 2
sensors_and_states = {
(sensor.original_name, hass.states.get(sensor.entity_id).state)
@@ -261,10 +277,12 @@ async def test_rm4_pro_no_sensor(hass):
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
+ mock_setup = await device.setup_entry(hass, mock_api=mock_api)
assert mock_api.check_sensors.call_count <= 1
- device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)})
+ device_entry = device_registry.async_get_device(
+ {(DOMAIN, mock_setup.entry.unique_id)}
+ )
entries = async_entries_for_device(entity_registry, device_entry.id)
sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN}
assert len(sensors) == 0
diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py
index 225cf5ce87a..b51577b5f3d 100644
--- a/tests/components/brother/test_sensor.py
+++ b/tests/components/brother/test_sensor.py
@@ -4,7 +4,11 @@ import json
from unittest.mock import Mock, patch
from homeassistant.components.brother.const import DOMAIN, UNIT_PAGES
-from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.sensor import (
+ ATTR_STATE_CLASS,
+ DOMAIN as SENSOR_DOMAIN,
+ STATE_CLASS_MEASUREMENT,
+)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
@@ -51,6 +55,7 @@ async def test_sensors(hass):
assert state
assert state.attributes.get(ATTR_ICON) == "mdi:printer"
assert state.state == "waiting"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.hl_l2340dw_status")
assert entry
@@ -61,6 +66,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "75"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_black_toner_remaining")
assert entry
@@ -71,6 +77,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "10"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_cyan_toner_remaining")
assert entry
@@ -81,6 +88,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "8"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_magenta_toner_remaining")
assert entry
@@ -91,6 +99,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "2"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_yellow_toner_remaining")
assert entry
@@ -103,6 +112,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_COUNTER) == 986
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "92"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_drum_remaining_life")
assert entry
@@ -115,6 +125,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_COUNTER) == 1611
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "92"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_black_drum_remaining_life")
assert entry
@@ -127,6 +138,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_COUNTER) == 1611
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "92"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_cyan_drum_remaining_life")
assert entry
@@ -139,6 +151,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_COUNTER) == 1611
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "92"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_magenta_drum_remaining_life")
assert entry
@@ -151,6 +164,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_COUNTER) == 1611
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "92"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_yellow_drum_remaining_life")
assert entry
@@ -161,6 +175,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_ICON) == "mdi:water-outline"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "97"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_fuser_remaining_life")
assert entry
@@ -171,6 +186,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_ICON) == "mdi:current-ac"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "97"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_belt_unit_remaining_life")
assert entry
@@ -181,6 +197,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "98"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_pf_kit_1_remaining_life")
assert entry
@@ -191,6 +208,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES
assert state.state == "986"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_page_counter")
assert entry
@@ -201,6 +219,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES
assert state.state == "538"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_duplex_unit_pages_counter")
assert entry
@@ -211,6 +230,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES
assert state.state == "709"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_b_w_counter")
assert entry
@@ -221,6 +241,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES
assert state.state == "902"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
entry = registry.async_get("sensor.hl_l2340dw_color_counter")
assert entry
@@ -232,6 +253,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP
assert state.state == "2019-09-24T12:14:56+00:00"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = registry.async_get("sensor.hl_l2340dw_uptime")
assert entry
diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py
index 5a5b7adf0e3..6993aa97081 100644
--- a/tests/components/caldav/test_calendar.py
+++ b/tests/components/caldav/test_calendar.py
@@ -222,6 +222,39 @@ 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):
+ """Set the default TZ to the one requested."""
+ return request.getfixturevalue(request.param)
+
+
+@pytest.fixture
+def utc():
+ """Set the default TZ to UTC."""
+ dt.set_default_time_zone(dt.get_time_zone("UTC"))
+
+
+@pytest.fixture
+def new_york():
+ """Set the default TZ to America/New_York."""
+ dt.set_default_time_zone(dt.get_time_zone("America/New_York"))
+
+
+@pytest.fixture
+def baghdad():
+ """Set the default TZ to Asia/Baghdad."""
+ dt.set_default_time_zone(dt.get_time_zone("Asia/Baghdad"))
+
@pytest.fixture(autouse=True)
def mock_http(hass):
@@ -524,30 +557,72 @@ async def test_no_result_with_filtering(mock_now, hass, calendar):
assert state.state == "off"
-@patch("homeassistant.util.dt.now", return_value=_local_datetime(17, 30))
-async def test_all_day_event_returned(mock_now, hass, calendar):
- """Test that the event lasting the whole day is returned."""
+async def _day_event_returned(hass, calendar, config, date_time):
+ with patch("homeassistant.util.dt.now", return_value=date_time):
+ assert await async_setup_component(hass, "calendar", {"calendar": config})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private_private")
+ assert state.name == calendar.name
+ assert state.state == STATE_ON
+ assert dict(state.attributes) == {
+ "friendly_name": "Private",
+ "message": "This is an all day event",
+ "all_day": True,
+ "offset_reached": False,
+ "start_time": "2017-11-27 00:00:00",
+ "end_time": "2017-11-28 00:00:00",
+ "location": "Hamburg",
+ "description": "What a beautiful day",
+ }
+
+
+@pytest.mark.parametrize("set_tz", ["utc", "new_york", "baghdad"], indirect=True)
+async def test_all_day_event_returned_early(hass, calendar, set_tz):
+ """Test that the event lasting the whole day is returned, if it's early in the local day."""
config = dict(CALDAV_CONFIG)
config["custom_calendars"] = [
{"name": "Private", "calendar": "Private", "search": ".*"}
]
- assert await async_setup_component(hass, "calendar", {"calendar": config})
- await hass.async_block_till_done()
+ await _day_event_returned(
+ hass,
+ calendar,
+ config,
+ datetime.datetime(2017, 11, 27, 0, 30).replace(tzinfo=dt.DEFAULT_TIME_ZONE),
+ )
- state = hass.states.get("calendar.private_private")
- assert state.name == calendar.name
- assert state.state == STATE_ON
- assert dict(state.attributes) == {
- "friendly_name": "Private",
- "message": "This is an all day event",
- "all_day": True,
- "offset_reached": False,
- "start_time": "2017-11-27 00:00:00",
- "end_time": "2017-11-28 00:00:00",
- "location": "Hamburg",
- "description": "What a beautiful day",
- }
+
+@pytest.mark.parametrize("set_tz", ["utc", "new_york", "baghdad"], indirect=True)
+async def test_all_day_event_returned_mid(hass, calendar, set_tz):
+ """Test that the event lasting the whole day is returned, if it's in the middle of the local day."""
+ config = dict(CALDAV_CONFIG)
+ config["custom_calendars"] = [
+ {"name": "Private", "calendar": "Private", "search": ".*"}
+ ]
+
+ await _day_event_returned(
+ hass,
+ calendar,
+ config,
+ datetime.datetime(2017, 11, 27, 12, 30).replace(tzinfo=dt.DEFAULT_TIME_ZONE),
+ )
+
+
+@pytest.mark.parametrize("set_tz", ["utc", "new_york", "baghdad"], indirect=True)
+async def test_all_day_event_returned_late(hass, calendar, set_tz):
+ """Test that the event lasting the whole day is returned, if it's late in the local day."""
+ config = dict(CALDAV_CONFIG)
+ config["custom_calendars"] = [
+ {"name": "Private", "calendar": "Private", "search": ".*"}
+ ]
+
+ await _day_event_returned(
+ hass,
+ calendar,
+ config,
+ datetime.datetime(2017, 11, 27, 23, 30).replace(tzinfo=dt.DEFAULT_TIME_ZONE),
+ )
@patch("homeassistant.util.dt.now", return_value=_local_datetime(21, 45))
@@ -655,33 +730,72 @@ async def test_event_rrule_endless(mock_now, hass, calendar):
}
-@patch(
- "homeassistant.util.dt.now",
- return_value=dt.as_local(datetime.datetime(2016, 12, 1, 17, 30)),
-)
-async def test_event_rrule_all_day(mock_now, hass, calendar):
- """Test that the recurring all day event is returned."""
+async def _event_rrule_all_day(hass, calendar, config, date_time):
+ with patch("homeassistant.util.dt.now", return_value=date_time):
+ assert await async_setup_component(hass, "calendar", {"calendar": config})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private_private")
+ assert state.name == calendar.name
+ assert state.state == STATE_ON
+ assert dict(state.attributes) == {
+ "friendly_name": "Private",
+ "message": "This is a recurring all day event",
+ "all_day": True,
+ "offset_reached": False,
+ "start_time": "2016-12-01 00:00:00",
+ "end_time": "2016-12-02 00:00:00",
+ "location": "Hamburg",
+ "description": "Groundhog Day",
+ }
+
+
+@pytest.mark.parametrize("set_tz", ["utc", "new_york", "baghdad"], indirect=True)
+async def test_event_rrule_all_day_early(hass, calendar, set_tz):
+ """Test that the recurring all day event is returned early in the local day, and not on the first occurrence."""
config = dict(CALDAV_CONFIG)
config["custom_calendars"] = [
{"name": "Private", "calendar": "Private", "search": ".*"}
]
- assert await async_setup_component(hass, "calendar", {"calendar": config})
- await hass.async_block_till_done()
+ await _event_rrule_all_day(
+ hass,
+ calendar,
+ config,
+ datetime.datetime(2016, 12, 1, 0, 30).replace(tzinfo=dt.DEFAULT_TIME_ZONE),
+ )
- state = hass.states.get("calendar.private_private")
- assert state.name == calendar.name
- assert state.state == STATE_ON
- assert dict(state.attributes) == {
- "friendly_name": "Private",
- "message": "This is a recurring all day event",
- "all_day": True,
- "offset_reached": False,
- "start_time": "2016-12-01 00:00:00",
- "end_time": "2016-12-02 00:00:00",
- "location": "Hamburg",
- "description": "Groundhog Day",
- }
+
+@pytest.mark.parametrize("set_tz", ["utc", "new_york", "baghdad"], indirect=True)
+async def test_event_rrule_all_day_mid(hass, calendar, set_tz):
+ """Test that the recurring all day event is returned in the middle of the local day, and not on the first occurrence."""
+ config = dict(CALDAV_CONFIG)
+ config["custom_calendars"] = [
+ {"name": "Private", "calendar": "Private", "search": ".*"}
+ ]
+
+ await _event_rrule_all_day(
+ hass,
+ calendar,
+ config,
+ datetime.datetime(2016, 12, 1, 17, 30).replace(tzinfo=dt.DEFAULT_TIME_ZONE),
+ )
+
+
+@pytest.mark.parametrize("set_tz", ["utc", "new_york", "baghdad"], indirect=True)
+async def test_event_rrule_all_day_late(hass, calendar, set_tz):
+ """Test that the recurring all day event is returned late in the local day, and not on the first occurrence."""
+ config = dict(CALDAV_CONFIG)
+ config["custom_calendars"] = [
+ {"name": "Private", "calendar": "Private", "search": ".*"}
+ ]
+
+ await _event_rrule_all_day(
+ hass,
+ calendar,
+ config,
+ datetime.datetime(2016, 12, 1, 23, 30).replace(tzinfo=dt.DEFAULT_TIME_ZONE),
+ )
@patch(
diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py
index 959d53184a4..3bb2b895c1a 100644
--- a/tests/components/cast/test_media_player.py
+++ b/tests/components/cast/test_media_player.py
@@ -10,7 +10,7 @@ import attr
import pychromecast
import pytest
-from homeassistant.components import tts
+from homeassistant.components import media_player, tts
from homeassistant.components.cast import media_player as cast
from homeassistant.components.cast.media_player import ChromecastInfo
from homeassistant.components.media_player.const import (
@@ -27,7 +27,7 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_SET,
)
from homeassistant.config import async_process_ha_core_config
-from homeassistant.const import EVENT_HOMEASSISTANT_STOP
+from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -702,8 +702,20 @@ async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock):
chromecast.start_app.assert_called_once_with("abc123")
# Play_media - cast with app name (quick play)
- await common.async_play_media(hass, "cast", '{"app_name": "youtube"}', entity_id)
- quick_play_mock.assert_called_once_with(ANY, "youtube", {})
+ await hass.services.async_call(
+ media_player.DOMAIN,
+ media_player.SERVICE_PLAY_MEDIA,
+ {
+ ATTR_ENTITY_ID: entity_id,
+ media_player.ATTR_MEDIA_CONTENT_TYPE: "cast",
+ media_player.ATTR_MEDIA_CONTENT_ID: '{"app_name":"youtube"}',
+ media_player.ATTR_MEDIA_EXTRA: {"metadata": {"metadatatype": 3}},
+ },
+ blocking=True,
+ )
+ quick_play_mock.assert_called_once_with(
+ ANY, "youtube", {"metadata": {"metadatatype": 3}}
+ )
async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock):
diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py
index d06742ba209..d93bdb5fae8 100644
--- a/tests/components/climacell/test_sensor.py
+++ b/tests/components/climacell/test_sensor.py
@@ -26,6 +26,7 @@ from tests.common import MockConfigEntry
_LOGGER = logging.getLogger(__name__)
CC_SENSOR_ENTITY_ID = "sensor.climacell_{}"
+O3 = "ozone"
CO = "carbon_monoxide"
NO2 = "nitrogen_dioxide"
SO2 = "sulfur_dioxide"
@@ -41,6 +42,49 @@ FIRE_INDEX = "fire_index"
GRASS_POLLEN = "grass_pollen_index"
WEED_POLLEN = "weed_pollen_index"
TREE_POLLEN = "tree_pollen_index"
+FEELS_LIKE = "feels_like"
+DEW_POINT = "dew_point"
+PRESSURE_SURFACE_LEVEL = "pressure_surface_level"
+SNOW_ACCUMULATION = "snow_accumulation"
+ICE_ACCUMULATION = "ice_accumulation"
+GHI = "global_horizontal_irradiance"
+CLOUD_BASE = "cloud_base"
+CLOUD_COVER = "cloud_cover"
+CLOUD_CEILING = "cloud_ceiling"
+WIND_GUST = "wind_gust"
+PRECIPITATION_TYPE = "precipitation_type"
+
+V3_FIELDS = [
+ O3,
+ CO,
+ NO2,
+ SO2,
+ PM25,
+ PM10,
+ MEP_AQI,
+ MEP_HEALTH_CONCERN,
+ MEP_PRIMARY_POLLUTANT,
+ EPA_AQI,
+ EPA_HEALTH_CONCERN,
+ EPA_PRIMARY_POLLUTANT,
+ FIRE_INDEX,
+ GRASS_POLLEN,
+ WEED_POLLEN,
+ TREE_POLLEN,
+]
+
+V4_FIELDS = [
+ *V3_FIELDS,
+ FEELS_LIKE,
+ DEW_POINT,
+ PRESSURE_SURFACE_LEVEL,
+ GHI,
+ CLOUD_BASE,
+ CLOUD_COVER,
+ CLOUD_CEILING,
+ WIND_GUST,
+ PRECIPITATION_TYPE,
+]
@callback
@@ -55,7 +99,9 @@ def _enable_entity(hass: HomeAssistant, entity_name: str) -> None:
assert updated_entry.disabled is False
-async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State:
+async def _setup(
+ hass: HomeAssistant, sensors: list[str], config: dict[str, Any]
+) -> State:
"""Set up entry and return entity state."""
with patch(
"homeassistant.util.dt.utcnow",
@@ -71,26 +117,10 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State:
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
- for entity_name in (
- CO,
- NO2,
- SO2,
- PM25,
- PM10,
- MEP_AQI,
- MEP_HEALTH_CONCERN,
- MEP_PRIMARY_POLLUTANT,
- EPA_AQI,
- EPA_HEALTH_CONCERN,
- EPA_PRIMARY_POLLUTANT,
- FIRE_INDEX,
- GRASS_POLLEN,
- WEED_POLLEN,
- TREE_POLLEN,
- ):
+ for entity_name in sensors:
_enable_entity(hass, CC_SENSOR_ENTITY_ID.format(entity_name))
await hass.async_block_till_done()
- assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 15
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == len(sensors)
def check_sensor_state(hass: HomeAssistant, entity_name: str, value: str):
@@ -106,7 +136,8 @@ async def test_v3_sensor(
climacell_config_entry_update: pytest.fixture,
) -> None:
"""Test v3 sensor data."""
- await _setup(hass, API_V3_ENTRY_DATA)
+ await _setup(hass, V3_FIELDS, API_V3_ENTRY_DATA)
+ check_sensor_state(hass, O3, "52.625")
check_sensor_state(hass, CO, "0.875")
check_sensor_state(hass, NO2, "14.1875")
check_sensor_state(hass, SO2, "2")
@@ -129,7 +160,8 @@ async def test_v4_sensor(
climacell_config_entry_update: pytest.fixture,
) -> None:
"""Test v4 sensor data."""
- await _setup(hass, API_V4_ENTRY_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")
@@ -145,3 +177,12 @@ async def test_v4_sensor(
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, "1.0")
+ 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/climate/test_device_action.py b/tests/components/climate/test_device_action.py
index dc956f0738c..3f9b1148443 100644
--- a/tests/components/climate/test_device_action.py
+++ b/tests/components/climate/test_device_action.py
@@ -30,7 +30,24 @@ def entity_reg(hass):
return mock_registry(hass)
-async def test_get_actions(hass, device_reg, entity_reg):
+@pytest.mark.parametrize(
+ "set_state,features_reg,features_state,expected_action_types",
+ [
+ (False, 0, 0, ["set_hvac_mode"]),
+ (False, const.SUPPORT_PRESET_MODE, 0, ["set_hvac_mode", "set_preset_mode"]),
+ (True, 0, 0, ["set_hvac_mode"]),
+ (True, 0, const.SUPPORT_PRESET_MODE, ["set_hvac_mode", "set_preset_mode"]),
+ ],
+)
+async def test_get_actions(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ features_reg,
+ features_state,
+ expected_action_types,
+):
"""Test we get the expected actions from a climate."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@@ -38,46 +55,30 @@ async def test_get_actions(hass, device_reg, entity_reg):
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- hass.states.async_set("climate.test_5678", const.HVAC_MODE_COOL, {})
- hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 17})
- expected_actions = [
- {
- "domain": DOMAIN,
- "type": "set_hvac_mode",
- "device_id": device_entry.id,
- "entity_id": "climate.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "set_preset_mode",
- "device_id": device_entry.id,
- "entity_id": "climate.test_5678",
- },
- ]
- actions = await async_get_device_automations(hass, "action", device_entry.id)
- assert_lists_same(actions, expected_actions)
-
-
-async def test_get_action_hvac_only(hass, device_reg, entity_reg):
- """Test we get the expected actions from a climate."""
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ supported_features=features_reg,
)
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- hass.states.async_set("climate.test_5678", const.HVAC_MODE_COOL, {})
- hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 1})
- expected_actions = [
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state}
+ )
+
+ expected_actions = []
+
+ expected_actions += [
{
"domain": DOMAIN,
- "type": "set_hvac_mode",
+ "type": action,
"device_id": device_entry.id,
- "entity_id": "climate.test_5678",
- },
+ "entity_id": f"{DOMAIN}.test_5678",
+ }
+ for action in expected_action_types
]
+
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert_lists_same(actions, expected_actions)
@@ -142,61 +143,153 @@ async def test_action(hass):
assert len(set_preset_mode_calls) == 1
-async def test_capabilities(hass):
+@pytest.mark.parametrize(
+ "set_state,capabilities_reg,capabilities_state,action,expected_capabilities",
+ [
+ (
+ False,
+ {const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF]},
+ {},
+ "set_hvac_mode",
+ [
+ {
+ "name": "hvac_mode",
+ "options": [("cool", "cool"), ("off", "off")],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ False,
+ {const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY]},
+ {},
+ "set_preset_mode",
+ [
+ {
+ "name": "preset_mode",
+ "options": [("home", "home"), ("away", "away")],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ True,
+ {},
+ {const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF]},
+ "set_hvac_mode",
+ [
+ {
+ "name": "hvac_mode",
+ "options": [("cool", "cool"), ("off", "off")],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ True,
+ {},
+ {const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY]},
+ "set_preset_mode",
+ [
+ {
+ "name": "preset_mode",
+ "options": [("home", "home"), ("away", "away")],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ ],
+)
+async def test_capabilities(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ capabilities_reg,
+ capabilities_state,
+ action,
+ expected_capabilities,
+):
"""Test getting capabilities."""
- hass.states.async_set(
- "climate.entity",
- const.HVAC_MODE_COOL,
- {
- const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF],
- const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY],
- },
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ capabilities=capabilities_reg,
+ )
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678",
+ const.HVAC_MODE_COOL,
+ capabilities_state,
+ )
- # Set HVAC mode
capabilities = await device_action.async_get_action_capabilities(
hass,
{
"domain": DOMAIN,
"device_id": "abcdefgh",
- "entity_id": "climate.entity",
- "type": "set_hvac_mode",
+ "entity_id": f"{DOMAIN}.test_5678",
+ "type": action,
},
)
assert capabilities and "extra_fields" in capabilities
- assert voluptuous_serialize.convert(
- capabilities["extra_fields"], custom_serializer=cv.custom_serializer
- ) == [
- {
- "name": "hvac_mode",
- "options": [("cool", "cool"), ("off", "off")],
- "required": True,
- "type": "select",
- }
- ]
+ assert (
+ voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ )
+ == expected_capabilities
+ )
+
+
+@pytest.mark.parametrize(
+ "action,capability_name",
+ [("set_hvac_mode", "hvac_mode"), ("set_preset_mode", "preset_mode")],
+)
+async def test_capabilities_missing_entity(
+ hass, device_reg, entity_reg, action, capability_name
+):
+ """Test getting capabilities."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
- # Set preset mode
capabilities = await device_action.async_get_action_capabilities(
hass,
{
"domain": DOMAIN,
"device_id": "abcdefgh",
- "entity_id": "climate.entity",
- "type": "set_preset_mode",
+ "entity_id": f"{DOMAIN}.test_5678",
+ "type": action,
},
)
- assert capabilities and "extra_fields" in capabilities
-
- assert voluptuous_serialize.convert(
- capabilities["extra_fields"], custom_serializer=cv.custom_serializer
- ) == [
+ expected_capabilities = [
{
- "name": "preset_mode",
- "options": [("home", "home"), ("away", "away")],
+ "name": capability_name,
+ "options": [],
"required": True,
"type": "select",
}
]
+
+ assert capabilities and "extra_fields" in capabilities
+
+ assert (
+ voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ )
+ == expected_capabilities
+ )
diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py
index 27341b6c2c9..2d6aa82fdf1 100644
--- a/tests/components/climate/test_device_condition.py
+++ b/tests/components/climate/test_device_condition.py
@@ -36,7 +36,24 @@ def calls(hass):
return async_mock_service(hass, "test", "automation")
-async def test_get_conditions(hass, device_reg, entity_reg):
+@pytest.mark.parametrize(
+ "set_state,features_reg,features_state,expected_condition_types",
+ [
+ (False, 0, 0, ["is_hvac_mode"]),
+ (False, const.SUPPORT_PRESET_MODE, 0, ["is_hvac_mode", "is_preset_mode"]),
+ (True, 0, 0, ["is_hvac_mode"]),
+ (True, 0, const.SUPPORT_PRESET_MODE, ["is_hvac_mode", "is_preset_mode"]),
+ ],
+)
+async def test_get_conditions(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ features_reg,
+ features_state,
+ expected_condition_types,
+):
"""Test we get the expected conditions from a climate."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@@ -44,64 +61,27 @@ async def test_get_conditions(hass, device_reg, entity_reg):
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- hass.states.async_set(
- f"{DOMAIN}.test_5678",
- const.HVAC_MODE_COOL,
- {
- const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL,
- const.ATTR_PRESET_MODE: const.PRESET_AWAY,
- const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY],
- },
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ supported_features=features_reg,
)
- hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 17})
- expected_conditions = [
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state}
+ )
+ expected_conditions = []
+ expected_conditions += [
{
"condition": "device",
"domain": DOMAIN,
- "type": "is_hvac_mode",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_preset_mode",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
- ]
- conditions = await async_get_device_automations(hass, "condition", device_entry.id)
- assert_lists_same(conditions, expected_conditions)
-
-
-async def test_get_conditions_hvac_only(hass, device_reg, entity_reg):
- """Test we get the expected conditions from a climate."""
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- hass.states.async_set(
- f"{DOMAIN}.test_5678",
- const.HVAC_MODE_COOL,
- {
- const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL,
- const.ATTR_PRESET_MODE: const.PRESET_AWAY,
- const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY],
- },
- )
- hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 1})
- expected_conditions = [
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_hvac_mode",
+ "type": condition,
"device_id": device_entry.id,
"entity_id": f"{DOMAIN}.test_5678",
}
+ for condition in expected_condition_types
]
conditions = await async_get_device_automations(hass, "condition", device_entry.id)
assert_lists_same(conditions, expected_conditions)
@@ -204,65 +184,155 @@ async def test_if_state(hass, calls):
assert len(calls) == 2
-async def test_capabilities(hass):
- """Bla."""
- hass.states.async_set(
- "climate.entity",
- const.HVAC_MODE_COOL,
- {
- const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL,
- const.ATTR_PRESET_MODE: const.PRESET_AWAY,
- const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF],
- const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY],
- },
+@pytest.mark.parametrize(
+ "set_state,capabilities_reg,capabilities_state,condition,expected_capabilities",
+ [
+ (
+ False,
+ {const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF]},
+ {},
+ "is_hvac_mode",
+ [
+ {
+ "name": "hvac_mode",
+ "options": [("cool", "cool"), ("off", "off")],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ False,
+ {const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY]},
+ {},
+ "is_preset_mode",
+ [
+ {
+ "name": "preset_mode",
+ "options": [("home", "home"), ("away", "away")],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ True,
+ {},
+ {const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF]},
+ "is_hvac_mode",
+ [
+ {
+ "name": "hvac_mode",
+ "options": [("cool", "cool"), ("off", "off")],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ True,
+ {},
+ {const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY]},
+ "is_preset_mode",
+ [
+ {
+ "name": "preset_mode",
+ "options": [("home", "home"), ("away", "away")],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ ],
+)
+async def test_capabilities(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ capabilities_reg,
+ capabilities_state,
+ condition,
+ expected_capabilities,
+):
+ """Test getting capabilities."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ capabilities=capabilities_reg,
+ )
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678",
+ const.HVAC_MODE_COOL,
+ capabilities_state,
+ )
- # Test hvac mode
capabilities = await device_condition.async_get_condition_capabilities(
hass,
{
"condition": "device",
"domain": DOMAIN,
- "device_id": "",
- "entity_id": "climate.entity",
- "type": "is_hvac_mode",
+ "device_id": "abcdefgh",
+ "entity_id": f"{DOMAIN}.test_5678",
+ "type": condition,
},
)
assert capabilities and "extra_fields" in capabilities
- assert voluptuous_serialize.convert(
- capabilities["extra_fields"], custom_serializer=cv.custom_serializer
- ) == [
- {
- "name": "hvac_mode",
- "options": [("cool", "cool"), ("off", "off")],
- "required": True,
- "type": "select",
- }
- ]
+ assert (
+ voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ )
+ == expected_capabilities
+ )
+
+
+@pytest.mark.parametrize(
+ "condition,capability_name",
+ [("is_hvac_mode", "hvac_mode"), ("is_preset_mode", "preset_mode")],
+)
+async def test_capabilities_missing_entity(
+ hass, device_reg, entity_reg, condition, capability_name
+):
+ """Test getting capabilities."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
- # Test preset mode
capabilities = await device_condition.async_get_condition_capabilities(
hass,
{
"condition": "device",
"domain": DOMAIN,
- "device_id": "",
- "entity_id": "climate.entity",
- "type": "is_preset_mode",
+ "device_id": "abcdefgh",
+ "entity_id": f"{DOMAIN}.test_5678",
+ "type": condition,
},
)
- assert capabilities and "extra_fields" in capabilities
-
- assert voluptuous_serialize.convert(
- capabilities["extra_fields"], custom_serializer=cv.custom_serializer
- ) == [
+ expected_capabilities = [
{
- "name": "preset_mode",
- "options": [("home", "home"), ("away", "away")],
+ "name": capability_name,
+ "options": [],
"required": True,
"type": "select",
}
]
+
+ assert capabilities and "extra_fields" in capabilities
+
+ assert (
+ voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ )
+ == expected_capabilities
+ )
diff --git a/tests/components/cloudflare/__init__.py b/tests/components/cloudflare/__init__.py
index 0e4e07b91cc..f2eaccab470 100644
--- a/tests/components/cloudflare/__init__.py
+++ b/tests/components/cloudflare/__init__.py
@@ -58,13 +58,21 @@ async def init_integration(
*,
data: dict = ENTRY_CONFIG,
options: dict = ENTRY_OPTIONS,
+ unique_id: str = MOCK_ZONE,
+ skip_setup: bool = False,
) -> MockConfigEntry:
"""Set up the Cloudflare integration in Home Assistant."""
- entry = MockConfigEntry(domain=DOMAIN, data=data, options=options)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=data,
+ options=options,
+ unique_id=unique_id,
+ )
entry.add_to_hass(hass)
- await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
+ if not skip_setup:
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
return entry
diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py
index 00dbb5e47df..230f4c3647f 100644
--- a/tests/components/cloudflare/test_config_flow.py
+++ b/tests/components/cloudflare/test_config_flow.py
@@ -6,7 +6,7 @@ from pycfdns.exceptions import (
)
from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN
-from homeassistant.config_entries import SOURCE_USER
+from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
from homeassistant.const import CONF_API_TOKEN, CONF_SOURCE, CONF_ZONE
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
@@ -162,3 +162,37 @@ async def test_user_form_single_instance_allowed(hass):
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "single_instance_allowed"
+
+
+async def test_reauth_flow(hass, cfupdate_flow):
+ """Test the reauthentication configuration flow."""
+ entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG)
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": SOURCE_REAUTH,
+ "unique_id": entry.unique_id,
+ "entry_id": entry.entry_id,
+ },
+ data=entry.data,
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth_confirm"
+
+ with _patch_async_setup_entry() as mock_setup_entry:
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_API_TOKEN: "other_token"},
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "reauth_successful"
+
+ assert entry.data[CONF_API_TOKEN] == "other_token"
+ assert entry.data[CONF_ZONE] == ENTRY_CONFIG[CONF_ZONE]
+ assert entry.data[CONF_RECORDS] == ENTRY_CONFIG[CONF_RECORDS]
+
+ assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py
index 5a42ca9f09c..ab7dbdab78e 100644
--- a/tests/components/cloudflare/test_init.py
+++ b/tests/components/cloudflare/test_init.py
@@ -1,8 +1,11 @@
"""Test the Cloudflare integration."""
-from pycfdns.exceptions import CloudflareConnectionException
+from pycfdns.exceptions import (
+ CloudflareAuthenticationException,
+ CloudflareConnectionException,
+)
from homeassistant.components.cloudflare.const import DOMAIN, SERVICE_UPDATE_RECORDS
-from homeassistant.config_entries import ConfigEntryState
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from . import ENTRY_CONFIG, init_integration
@@ -36,6 +39,30 @@ async def test_async_setup_raises_entry_not_ready(hass, cfupdate):
assert entry.state is ConfigEntryState.SETUP_RETRY
+async def test_async_setup_raises_entry_auth_failed(hass, cfupdate):
+ """Test that it throws ConfigEntryAuthFailed when exception occurs during setup."""
+ instance = cfupdate.return_value
+
+ entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG)
+ entry.add_to_hass(hass)
+
+ instance.get_zone_id.side_effect = CloudflareAuthenticationException()
+ await hass.config_entries.async_setup(entry.entry_id)
+
+ assert entry.state is ConfigEntryState.SETUP_ERROR
+
+ flows = hass.config_entries.flow.async_progress()
+ assert len(flows) == 1
+
+ flow = flows[0]
+ assert flow["step_id"] == "reauth_confirm"
+ assert flow["handler"] == DOMAIN
+
+ assert "context" in flow
+ assert flow["context"]["source"] == SOURCE_REAUTH
+ assert flow["context"]["entry_id"] == entry.entry_id
+
+
async def test_integration_services(hass, cfupdate):
"""Test integration services."""
instance = cfupdate.return_value
diff --git a/tests/components/coinbase/__init__.py b/tests/components/coinbase/__init__.py
new file mode 100644
index 00000000000..d3629804954
--- /dev/null
+++ b/tests/components/coinbase/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Coinbase integration."""
diff --git a/tests/components/coinbase/common.py b/tests/components/coinbase/common.py
new file mode 100644
index 00000000000..5fcab6605bd
--- /dev/null
+++ b/tests/components/coinbase/common.py
@@ -0,0 +1,84 @@
+"""Collection of helpers."""
+from homeassistant.components.coinbase.const import (
+ CONF_CURRENCIES,
+ CONF_EXCHANGE_RATES,
+ DOMAIN,
+)
+from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
+
+from .const import GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2, MOCK_ACCOUNTS_RESPONSE
+
+from tests.common import MockConfigEntry
+
+
+class MockPagination:
+ """Mock pagination result."""
+
+ def __init__(self, value=None):
+ """Load simple pagination for tests."""
+ self.next_starting_after = value
+
+
+class MockGetAccounts:
+ """Mock accounts with pagination."""
+
+ def __init__(self, starting_after=0):
+ """Init mocked object, forced to return two at a time."""
+ if (target_end := starting_after + 2) >= (
+ max_end := len(MOCK_ACCOUNTS_RESPONSE)
+ ):
+ end = max_end
+ self.pagination = MockPagination(value=None)
+ else:
+ end = target_end
+ self.pagination = MockPagination(value=target_end)
+
+ self.accounts = {
+ "data": MOCK_ACCOUNTS_RESPONSE[starting_after:end],
+ }
+ self.started_at = starting_after
+
+ def __getitem__(self, item):
+ """Handle subscript request."""
+ return self.accounts[item]
+
+
+def mocked_get_accounts(_, **kwargs):
+ """Return simplied accounts using mock."""
+ return MockGetAccounts(**kwargs)
+
+
+def mock_get_current_user():
+ """Return a simplified mock user."""
+ return {
+ "id": "123456-abcdef",
+ "name": "Test User",
+ }
+
+
+def mock_get_exchange_rates():
+ """Return a heavily reduced mock list of exchange rates for testing."""
+ return {
+ "currency": "USD",
+ "rates": {GOOD_EXCHNAGE_RATE_2: "0.109", GOOD_EXCHNAGE_RATE: "0.00002"},
+ }
+
+
+async def init_mock_coinbase(hass):
+ """Init Coinbase integration for testing."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="abcde12345",
+ title="Test User",
+ data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"},
+ options={
+ CONF_CURRENCIES: [],
+ CONF_EXCHANGE_RATES: [],
+ },
+ )
+ config_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ return config_entry
diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py
new file mode 100644
index 00000000000..864ebc18701
--- /dev/null
+++ b/tests/components/coinbase/const.py
@@ -0,0 +1,33 @@
+"""Constants for testing the Coinbase integration."""
+
+GOOD_CURRENCY = "BTC"
+GOOD_CURRENCY_2 = "USD"
+GOOD_CURRENCY_3 = "EUR"
+GOOD_EXCHNAGE_RATE = "BTC"
+GOOD_EXCHNAGE_RATE_2 = "ATOM"
+BAD_CURRENCY = "ETH"
+BAD_EXCHANGE_RATE = "ETH"
+
+MOCK_ACCOUNTS_RESPONSE = [
+ {
+ "balance": {"amount": "13.38", "currency": GOOD_CURRENCY_3},
+ "currency": GOOD_CURRENCY_3,
+ "id": "ABCDEF",
+ "name": "BTC Wallet",
+ "native_balance": {"amount": "15.02", "currency": GOOD_CURRENCY_2},
+ },
+ {
+ "balance": {"amount": "0.00001", "currency": GOOD_CURRENCY},
+ "currency": GOOD_CURRENCY,
+ "id": "123456789",
+ "name": "BTC Wallet",
+ "native_balance": {"amount": "100.12", "currency": GOOD_CURRENCY_2},
+ },
+ {
+ "balance": {"amount": "9.90", "currency": GOOD_CURRENCY_2},
+ "currency": "USD",
+ "id": "987654321",
+ "name": "USD Wallet",
+ "native_balance": {"amount": "9.90", "currency": GOOD_CURRENCY_2},
+ },
+]
diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py
new file mode 100644
index 00000000000..4c7b6c13333
--- /dev/null
+++ b/tests/components/coinbase/test_config_flow.py
@@ -0,0 +1,359 @@
+"""Test the Coinbase config flow."""
+from unittest.mock import patch
+
+from coinbase.wallet.error import AuthenticationError
+from requests.models import Response
+
+from homeassistant import config_entries, setup
+from homeassistant.components.coinbase.const import (
+ CONF_CURRENCIES,
+ CONF_EXCHANGE_RATES,
+ CONF_YAML_API_TOKEN,
+ DOMAIN,
+)
+from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
+
+from .common import (
+ init_mock_coinbase,
+ mock_get_current_user,
+ mock_get_exchange_rates,
+ mocked_get_accounts,
+)
+from .const import (
+ BAD_CURRENCY,
+ BAD_EXCHANGE_RATE,
+ GOOD_CURRENCY,
+ GOOD_CURRENCY_2,
+ GOOD_EXCHNAGE_RATE,
+ GOOD_EXCHNAGE_RATE_2,
+)
+
+from tests.common import MockConfigEntry
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "coinbase.wallet.client.Client.get_current_user",
+ return_value=mock_get_current_user(),
+ ), patch(
+ "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
+ ), patch(
+ "coinbase.wallet.client.Client.get_exchange_rates",
+ return_value=mock_get_exchange_rates(),
+ ), patch(
+ "homeassistant.components.coinbase.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.coinbase.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_API_KEY: "123456",
+ CONF_API_TOKEN: "AbCDeF",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Test User"
+ assert result2["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_invalid_auth(hass):
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ response = Response()
+ response.status_code = 401
+ api_auth_error = AuthenticationError(
+ response,
+ "authentication_error",
+ "invalid signature",
+ [{"id": "authentication_error", "message": "invalid signature"}],
+ )
+ with patch(
+ "coinbase.wallet.client.Client.get_current_user",
+ side_effect=api_auth_error,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_API_KEY: "123456",
+ CONF_API_TOKEN: "AbCDeF",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_form_cannot_connect(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "coinbase.wallet.client.Client.get_current_user",
+ side_effect=ConnectionError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_API_KEY: "123456",
+ CONF_API_TOKEN: "AbCDeF",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_form_catch_all_exception(hass):
+ """Test we handle unknown exceptions."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "coinbase.wallet.client.Client.get_current_user",
+ side_effect=Exception,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_API_KEY: "123456",
+ CONF_API_TOKEN: "AbCDeF",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_option_good_account_currency(hass):
+ """Test we handle a good wallet currency option."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ entry_id="abcde12345",
+ title="Test User",
+ data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"},
+ options={
+ CONF_CURRENCIES: [GOOD_CURRENCY_2],
+ CONF_EXCHANGE_RATES: [],
+ },
+ )
+ config_entry.add_to_hass(hass)
+
+ with patch(
+ "coinbase.wallet.client.Client.get_current_user",
+ return_value=mock_get_current_user(),
+ ), patch(
+ "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
+ ), patch(
+ "coinbase.wallet.client.Client.get_exchange_rates",
+ return_value=mock_get_exchange_rates(),
+ ):
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ await hass.async_block_till_done()
+ result2 = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_CURRENCIES: [GOOD_CURRENCY],
+ CONF_EXCHANGE_RATES: [],
+ },
+ )
+ assert result2["type"] == "create_entry"
+
+
+async def test_form_bad_account_currency(hass):
+ """Test we handle a bad currency option."""
+ with patch(
+ "coinbase.wallet.client.Client.get_current_user",
+ return_value=mock_get_current_user(),
+ ), patch(
+ "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
+ ), patch(
+ "coinbase.wallet.client.Client.get_exchange_rates",
+ return_value=mock_get_exchange_rates(),
+ ):
+ config_entry = await init_mock_coinbase(hass)
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ await hass.async_block_till_done()
+ result2 = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_CURRENCIES: [BAD_CURRENCY],
+ CONF_EXCHANGE_RATES: [],
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "currency_unavaliable"}
+
+
+async def test_option_good_exchange_rate(hass):
+ """Test we handle a good exchange rate option."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ entry_id="abcde12345",
+ title="Test User",
+ data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"},
+ options={
+ CONF_CURRENCIES: [],
+ CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE_2],
+ },
+ )
+ config_entry.add_to_hass(hass)
+
+ with patch(
+ "coinbase.wallet.client.Client.get_current_user",
+ return_value=mock_get_current_user(),
+ ), patch(
+ "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
+ ), patch(
+ "coinbase.wallet.client.Client.get_exchange_rates",
+ return_value=mock_get_exchange_rates(),
+ ):
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ await hass.async_block_till_done()
+ result2 = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_CURRENCIES: [],
+ CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE],
+ },
+ )
+ assert result2["type"] == "create_entry"
+
+
+async def test_form_bad_exchange_rate(hass):
+ """Test we handle a bad exchange rate."""
+ with patch(
+ "coinbase.wallet.client.Client.get_current_user",
+ return_value=mock_get_current_user(),
+ ), patch(
+ "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
+ ), patch(
+ "coinbase.wallet.client.Client.get_exchange_rates",
+ return_value=mock_get_exchange_rates(),
+ ):
+ config_entry = await init_mock_coinbase(hass)
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ await hass.async_block_till_done()
+ result2 = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_CURRENCIES: [],
+ CONF_EXCHANGE_RATES: [BAD_EXCHANGE_RATE],
+ },
+ )
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "exchange_rate_unavaliable"}
+
+
+async def test_option_catch_all_exception(hass):
+ """Test we handle an unknown exception in the option flow."""
+ with patch(
+ "coinbase.wallet.client.Client.get_current_user",
+ return_value=mock_get_current_user(),
+ ), patch(
+ "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
+ ), patch(
+ "coinbase.wallet.client.Client.get_exchange_rates",
+ return_value=mock_get_exchange_rates(),
+ ):
+ config_entry = await init_mock_coinbase(hass)
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ with patch(
+ "coinbase.wallet.client.Client.get_accounts",
+ side_effect=Exception,
+ ):
+ result2 = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_CURRENCIES: [],
+ CONF_EXCHANGE_RATES: ["ETH"],
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_yaml_import(hass):
+ """Test YAML import works."""
+ conf = {
+ CONF_API_KEY: "123456",
+ CONF_YAML_API_TOKEN: "AbCDeF",
+ CONF_CURRENCIES: ["BTC", "USD"],
+ CONF_EXCHANGE_RATES: ["ATOM", "BTC"],
+ }
+ with patch(
+ "coinbase.wallet.client.Client.get_current_user",
+ return_value=mock_get_current_user(),
+ ), patch(
+ "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
+ ), patch(
+ "coinbase.wallet.client.Client.get_exchange_rates",
+ return_value=mock_get_exchange_rates(),
+ ), patch(
+ "homeassistant.components.coinbase.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.coinbase.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
+ )
+ assert result["type"] == "create_entry"
+ assert result["title"] == "Test User"
+ assert result["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}
+ assert result["options"] == {
+ CONF_CURRENCIES: ["BTC", "USD"],
+ CONF_EXCHANGE_RATES: ["ATOM", "BTC"],
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_yaml_existing(hass):
+ """Test YAML ignored when already processed."""
+ MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_API_KEY: "123456",
+ CONF_API_TOKEN: "AbCDeF",
+ },
+ ).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_API_KEY: "123456",
+ CONF_YAML_API_TOKEN: "AbCDeF",
+ },
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
diff --git a/tests/components/coinbase/test_init.py b/tests/components/coinbase/test_init.py
new file mode 100644
index 00000000000..612519b1cee
--- /dev/null
+++ b/tests/components/coinbase/test_init.py
@@ -0,0 +1,80 @@
+"""Test the Coinbase integration."""
+from unittest.mock import patch
+
+from homeassistant import config_entries
+from homeassistant.components.coinbase.const import (
+ CONF_CURRENCIES,
+ CONF_EXCHANGE_RATES,
+ CONF_YAML_API_TOKEN,
+ DOMAIN,
+)
+from homeassistant.const import CONF_API_KEY
+from homeassistant.setup import async_setup_component
+
+from .common import (
+ init_mock_coinbase,
+ mock_get_current_user,
+ mock_get_exchange_rates,
+ mocked_get_accounts,
+)
+from .const import (
+ GOOD_CURRENCY,
+ GOOD_CURRENCY_2,
+ GOOD_EXCHNAGE_RATE,
+ GOOD_EXCHNAGE_RATE_2,
+)
+
+
+async def test_setup(hass):
+ """Test setting up from configuration.yaml."""
+ conf = {
+ DOMAIN: {
+ CONF_API_KEY: "123456",
+ CONF_YAML_API_TOKEN: "AbCDeF",
+ CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2],
+ CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2],
+ }
+ }
+ with patch(
+ "coinbase.wallet.client.Client.get_current_user",
+ return_value=mock_get_current_user(),
+ ), patch(
+ "coinbase.wallet.client.Client.get_accounts",
+ new=mocked_get_accounts,
+ ), patch(
+ "coinbase.wallet.client.Client.get_exchange_rates",
+ return_value=mock_get_exchange_rates(),
+ ):
+ assert await async_setup_component(hass, DOMAIN, conf)
+ entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(entries) == 1
+ assert entries[0].title == "Test User"
+ assert entries[0].source == config_entries.SOURCE_IMPORT
+ assert entries[0].options == {
+ CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2],
+ CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2],
+ }
+
+
+async def test_unload_entry(hass):
+ """Test successful unload of entry."""
+ with patch(
+ "coinbase.wallet.client.Client.get_current_user",
+ return_value=mock_get_current_user(),
+ ), patch(
+ "coinbase.wallet.client.Client.get_accounts",
+ new=mocked_get_accounts,
+ ), patch(
+ "coinbase.wallet.client.Client.get_exchange_rates",
+ return_value=mock_get_exchange_rates(),
+ ):
+ entry = await init_mock_coinbase(hass)
+
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ assert entry.state == config_entries.ConfigEntryState.LOADED
+
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert entry.state == config_entries.ConfigEntryState.NOT_LOADED
+ assert not hass.data.get(DOMAIN)
diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py
index 60bb4e5401b..0dc76aa7e61 100644
--- a/tests/components/cover/test_device_action.py
+++ b/tests/components/cover/test_device_action.py
@@ -2,7 +2,16 @@
import pytest
import homeassistant.components.automation as automation
-from homeassistant.components.cover import DOMAIN
+from homeassistant.components.cover import (
+ DOMAIN,
+ SUPPORT_CLOSE,
+ SUPPORT_CLOSE_TILT,
+ SUPPORT_OPEN,
+ SUPPORT_OPEN_TILT,
+ SUPPORT_SET_POSITION,
+ SUPPORT_SET_TILT_POSITION,
+ SUPPORT_STOP,
+)
from homeassistant.const import CONF_PLATFORM
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
@@ -31,56 +40,37 @@ def entity_reg(hass):
return mock_registry(hass)
-async def test_get_actions(hass, device_reg, entity_reg, enable_custom_integrations):
- """Test we get the expected actions from a cover."""
- platform = getattr(hass.components, f"test.{DOMAIN}")
- platform.init()
- ent = platform.ENTITIES[0]
-
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(
- DOMAIN, "test", ent.unique_id, device_id=device_entry.id
- )
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
- await hass.async_block_till_done()
-
- expected_actions = [
- {
- "domain": DOMAIN,
- "type": "open",
- "device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
- {
- "domain": DOMAIN,
- "type": "close",
- "device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
- {
- "domain": DOMAIN,
- "type": "stop",
- "device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
- ]
- actions = await async_get_device_automations(hass, "action", device_entry.id)
- assert_lists_same(actions, expected_actions)
-
-
-async def test_get_actions_tilt(
- hass, device_reg, entity_reg, enable_custom_integrations
+@pytest.mark.parametrize(
+ "set_state,features_reg,features_state,expected_action_types",
+ [
+ (False, 0, 0, []),
+ (False, SUPPORT_CLOSE_TILT, 0, ["close_tilt"]),
+ (False, SUPPORT_CLOSE, 0, ["close"]),
+ (False, SUPPORT_OPEN_TILT, 0, ["open_tilt"]),
+ (False, SUPPORT_OPEN, 0, ["open"]),
+ (False, SUPPORT_SET_POSITION, 0, ["set_position"]),
+ (False, SUPPORT_SET_TILT_POSITION, 0, ["set_tilt_position"]),
+ (False, SUPPORT_STOP, 0, ["stop"]),
+ (True, 0, 0, []),
+ (True, 0, SUPPORT_CLOSE_TILT, ["close_tilt"]),
+ (True, 0, SUPPORT_CLOSE, ["close"]),
+ (True, 0, SUPPORT_OPEN_TILT, ["open_tilt"]),
+ (True, 0, SUPPORT_OPEN, ["open"]),
+ (True, 0, SUPPORT_SET_POSITION, ["set_position"]),
+ (True, 0, SUPPORT_SET_TILT_POSITION, ["set_tilt_position"]),
+ (True, 0, SUPPORT_STOP, ["stop"]),
+ ],
+)
+async def test_get_actions(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ features_reg,
+ features_state,
+ expected_action_types,
):
"""Test we get the expected actions from a cover."""
- platform = getattr(hass.components, f"test.{DOMAIN}")
- platform.init()
- ent = platform.ENTITIES[3]
-
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
@@ -88,124 +78,27 @@ async def test_get_actions_tilt(
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_reg.async_get_or_create(
- DOMAIN, "test", ent.unique_id, device_id=device_entry.id
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ supported_features=features_reg,
)
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state}
+ )
await hass.async_block_till_done()
- expected_actions = [
+ expected_actions = []
+ expected_actions += [
{
"domain": DOMAIN,
- "type": "open",
+ "type": action,
"device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
- {
- "domain": DOMAIN,
- "type": "close",
- "device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
- {
- "domain": DOMAIN,
- "type": "stop",
- "device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
- {
- "domain": DOMAIN,
- "type": "open_tilt",
- "device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
- {
- "domain": DOMAIN,
- "type": "close_tilt",
- "device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
- ]
- actions = await async_get_device_automations(hass, "action", device_entry.id)
- assert_lists_same(actions, expected_actions)
-
-
-async def test_get_actions_set_pos(
- hass, device_reg, entity_reg, enable_custom_integrations
-):
- """Test we get the expected actions from a cover."""
- platform = getattr(hass.components, f"test.{DOMAIN}")
- platform.init()
- ent = platform.ENTITIES[1]
-
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(
- DOMAIN, "test", ent.unique_id, device_id=device_entry.id
- )
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
- await hass.async_block_till_done()
-
- expected_actions = [
- {
- "domain": DOMAIN,
- "type": "set_position",
- "device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
- ]
- actions = await async_get_device_automations(hass, "action", device_entry.id)
- assert_lists_same(actions, expected_actions)
-
-
-async def test_get_actions_set_tilt_pos(
- hass, device_reg, entity_reg, enable_custom_integrations
-):
- """Test we get the expected actions from a cover."""
- platform = getattr(hass.components, f"test.{DOMAIN}")
- platform.init()
- ent = platform.ENTITIES[2]
-
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(
- DOMAIN, "test", ent.unique_id, device_id=device_entry.id
- )
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
- await hass.async_block_till_done()
-
- expected_actions = [
- {
- "domain": DOMAIN,
- "type": "open",
- "device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
- {
- "domain": DOMAIN,
- "type": "close",
- "device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
- {
- "domain": DOMAIN,
- "type": "stop",
- "device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
- {
- "domain": DOMAIN,
- "type": "set_tilt_position",
- "device_id": device_entry.id,
- "entity_id": ent.entity_id,
- },
+ "entity_id": f"{DOMAIN}.test_5678",
+ }
+ for action in expected_action_types
]
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert_lists_same(actions, expected_actions)
diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py
index 8e8d92e5a1c..cf05a112a0a 100644
--- a/tests/components/cover/test_device_condition.py
+++ b/tests/components/cover/test_device_condition.py
@@ -2,7 +2,13 @@
import pytest
import homeassistant.components.automation as automation
-from homeassistant.components.cover import DOMAIN
+from homeassistant.components.cover import (
+ DOMAIN,
+ SUPPORT_CLOSE,
+ SUPPORT_OPEN,
+ SUPPORT_SET_POSITION,
+ SUPPORT_SET_TILT_POSITION,
+)
from homeassistant.const import (
CONF_PLATFORM,
STATE_CLOSED,
@@ -43,65 +49,31 @@ def calls(hass):
return async_mock_service(hass, "test", "automation")
-async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integrations):
- """Test we get the expected conditions from a cover."""
- platform = getattr(hass.components, f"test.{DOMAIN}")
- platform.init()
- ent = platform.ENTITIES[0]
-
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(
- DOMAIN, "test", ent.unique_id, device_id=device_entry.id
- )
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
-
- expected_conditions = [
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_open",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_closed",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_opening",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_closing",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- ]
- conditions = await async_get_device_automations(hass, "condition", device_entry.id)
- assert_lists_same(conditions, expected_conditions)
-
-
-async def test_get_conditions_set_pos(
- hass, device_reg, entity_reg, enable_custom_integrations
+@pytest.mark.parametrize(
+ "set_state,features_reg,features_state,expected_condition_types",
+ [
+ (False, 0, 0, []),
+ (False, SUPPORT_CLOSE, 0, ["is_open", "is_closed", "is_opening", "is_closing"]),
+ (False, SUPPORT_OPEN, 0, ["is_open", "is_closed", "is_opening", "is_closing"]),
+ (False, SUPPORT_SET_POSITION, 0, ["is_position"]),
+ (False, SUPPORT_SET_TILT_POSITION, 0, ["is_tilt_position"]),
+ (True, 0, 0, []),
+ (True, 0, SUPPORT_CLOSE, ["is_open", "is_closed", "is_opening", "is_closing"]),
+ (True, 0, SUPPORT_OPEN, ["is_open", "is_closed", "is_opening", "is_closing"]),
+ (True, 0, SUPPORT_SET_POSITION, ["is_position"]),
+ (True, 0, SUPPORT_SET_TILT_POSITION, ["is_tilt_position"]),
+ ],
+)
+async def test_get_conditions(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ features_reg,
+ features_state,
+ expected_condition_types,
):
"""Test we get the expected conditions from a cover."""
- platform = getattr(hass.components, f"test.{DOMAIN}")
- platform.init()
- ent = platform.ENTITIES[1]
-
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
@@ -109,106 +81,28 @@ async def test_get_conditions_set_pos(
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_reg.async_get_or_create(
- DOMAIN, "test", ent.unique_id, device_id=device_entry.id
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ supported_features=features_reg,
)
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state}
+ )
+ await hass.async_block_till_done()
- expected_conditions = [
+ expected_conditions = []
+ expected_conditions += [
{
"condition": "device",
"domain": DOMAIN,
- "type": "is_open",
+ "type": condition,
"device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_closed",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_opening",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_closing",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_position",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- ]
- conditions = await async_get_device_automations(hass, "condition", device_entry.id)
- assert_lists_same(conditions, expected_conditions)
-
-
-async def test_get_conditions_set_tilt_pos(
- hass, device_reg, entity_reg, enable_custom_integrations
-):
- """Test we get the expected conditions from a cover."""
- platform = getattr(hass.components, f"test.{DOMAIN}")
- platform.init()
- ent = platform.ENTITIES[2]
-
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(
- DOMAIN, "test", ent.unique_id, device_id=device_entry.id
- )
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
-
- expected_conditions = [
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_open",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_closed",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_opening",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_closing",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_tilt_position",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
+ "entity_id": f"{DOMAIN}.test_5678",
+ }
+ for condition in expected_condition_types
]
conditions = await async_get_device_automations(hass, "condition", device_entry.id)
assert_lists_same(conditions, expected_conditions)
diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py
index 8732fcc8020..b7f15de1e3c 100644
--- a/tests/components/cover/test_device_trigger.py
+++ b/tests/components/cover/test_device_trigger.py
@@ -4,7 +4,12 @@ from datetime import timedelta
import pytest
import homeassistant.components.automation as automation
-from homeassistant.components.cover import DOMAIN
+from homeassistant.components.cover import (
+ DOMAIN,
+ SUPPORT_OPEN,
+ SUPPORT_SET_POSITION,
+ SUPPORT_SET_TILT_POSITION,
+)
from homeassistant.const import (
CONF_PLATFORM,
STATE_CLOSED,
@@ -47,65 +52,47 @@ def calls(hass):
return async_mock_service(hass, "test", "automation")
-async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrations):
- """Test we get the expected triggers from a cover."""
- platform = getattr(hass.components, f"test.{DOMAIN}")
- platform.init()
- ent = platform.ENTITIES[0]
-
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(
- DOMAIN, "test", ent.unique_id, device_id=device_entry.id
- )
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
-
- expected_triggers = [
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "opened",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "closed",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "opening",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "closing",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- ]
- triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
- assert_lists_same(triggers, expected_triggers)
-
-
-async def test_get_triggers_set_pos(
- hass, device_reg, entity_reg, enable_custom_integrations
+@pytest.mark.parametrize(
+ "set_state,features_reg,features_state,expected_trigger_types",
+ [
+ (False, SUPPORT_OPEN, 0, ["opened", "closed", "opening", "closing"]),
+ (
+ False,
+ SUPPORT_OPEN | SUPPORT_SET_POSITION,
+ 0,
+ ["opened", "closed", "opening", "closing", "position"],
+ ),
+ (
+ False,
+ SUPPORT_OPEN | SUPPORT_SET_TILT_POSITION,
+ 0,
+ ["opened", "closed", "opening", "closing", "tilt_position"],
+ ),
+ (True, 0, SUPPORT_OPEN, ["opened", "closed", "opening", "closing"]),
+ (
+ True,
+ 0,
+ SUPPORT_OPEN | SUPPORT_SET_POSITION,
+ ["opened", "closed", "opening", "closing", "position"],
+ ),
+ (
+ True,
+ 0,
+ SUPPORT_OPEN | SUPPORT_SET_TILT_POSITION,
+ ["opened", "closed", "opening", "closing", "tilt_position"],
+ ),
+ ],
+)
+async def test_get_triggers(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ features_reg,
+ features_state,
+ expected_trigger_types,
):
"""Test we get the expected triggers from a cover."""
- platform = getattr(hass.components, f"test.{DOMAIN}")
- platform.init()
- ent = platform.ENTITIES[1]
-
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
@@ -113,106 +100,30 @@ async def test_get_triggers_set_pos(
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_reg.async_get_or_create(
- DOMAIN, "test", ent.unique_id, device_id=device_entry.id
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ supported_features=features_reg,
)
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678",
+ "attributes",
+ {"supported_features": features_state},
+ )
- expected_triggers = [
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "opened",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "closed",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "opening",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "closing",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "position",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- ]
- triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
- assert_lists_same(triggers, expected_triggers)
+ expected_triggers = []
-
-async def test_get_triggers_set_tilt_pos(
- hass, device_reg, entity_reg, enable_custom_integrations
-):
- """Test we get the expected triggers from a cover."""
- platform = getattr(hass.components, f"test.{DOMAIN}")
- platform.init()
- ent = platform.ENTITIES[2]
-
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(
- DOMAIN, "test", ent.unique_id, device_id=device_entry.id
- )
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
-
- expected_triggers = [
+ expected_triggers += [
{
"platform": "device",
"domain": DOMAIN,
- "type": "opened",
+ "type": trigger,
"device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "closed",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "opening",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "closing",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
- {
- "platform": "device",
- "domain": DOMAIN,
- "type": "tilt_position",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_{ent.unique_id}",
- },
+ "entity_id": f"{DOMAIN}.test_5678",
+ }
+ for trigger in expected_trigger_types
]
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
assert_lists_same(triggers, expected_triggers)
diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py
index 6583372d7bd..814ec588b1e 100644
--- a/tests/components/deconz/test_init.py
+++ b/tests/components/deconz/test_init.py
@@ -61,8 +61,8 @@ async def test_setup_entry_successful(hass, aioclient_mock):
config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert hass.data[DECONZ_DOMAIN]
- assert config_entry.unique_id in hass.data[DECONZ_DOMAIN]
- assert hass.data[DECONZ_DOMAIN][config_entry.unique_id].master
+ assert config_entry.entry_id in hass.data[DECONZ_DOMAIN]
+ assert hass.data[DECONZ_DOMAIN][config_entry.entry_id].master
async def test_setup_entry_multiple_gateways(hass, aioclient_mock):
@@ -80,8 +80,8 @@ async def test_setup_entry_multiple_gateways(hass, aioclient_mock):
)
assert len(hass.data[DECONZ_DOMAIN]) == 2
- assert hass.data[DECONZ_DOMAIN][config_entry.unique_id].master
- assert not hass.data[DECONZ_DOMAIN][config_entry2.unique_id].master
+ assert hass.data[DECONZ_DOMAIN][config_entry.entry_id].master
+ assert not hass.data[DECONZ_DOMAIN][config_entry2.entry_id].master
async def test_unload_entry(hass, aioclient_mock):
@@ -112,7 +112,7 @@ async def test_unload_entry_multiple_gateways(hass, aioclient_mock):
assert await async_unload_entry(hass, config_entry)
assert len(hass.data[DECONZ_DOMAIN]) == 1
- assert hass.data[DECONZ_DOMAIN][config_entry2.unique_id].master
+ assert hass.data[DECONZ_DOMAIN][config_entry2.entry_id].master
async def test_update_group_unique_id(hass):
diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py
index a5e27709ebf..2beb339dd4f 100644
--- a/tests/components/deconz/test_light.py
+++ b/tests/components/deconz/test_light.py
@@ -4,22 +4,36 @@ from unittest.mock import patch
import pytest
-from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS
+from homeassistant.components.deconz.const import ATTR_ON, CONF_ALLOW_DECONZ_GROUPS
+from homeassistant.components.deconz.light import DECONZ_GROUP
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
+ ATTR_COLOR_MODE,
ATTR_COLOR_TEMP,
ATTR_EFFECT,
+ ATTR_EFFECT_LIST,
ATTR_FLASH,
ATTR_HS_COLOR,
ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS,
+ ATTR_RGB_COLOR,
+ ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION,
+ ATTR_XY_COLOR,
+ COLOR_MODE_BRIGHTNESS,
+ COLOR_MODE_COLOR_TEMP,
+ COLOR_MODE_HS,
+ COLOR_MODE_ONOFF,
+ COLOR_MODE_XY,
DOMAIN as LIGHT_DOMAIN,
EFFECT_COLORLOOP,
FLASH_LONG,
FLASH_SHORT,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
+ SUPPORT_EFFECT,
+ SUPPORT_FLASH,
+ SUPPORT_TRANSITION,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -42,27 +56,667 @@ async def test_no_lights_or_groups(hass, aioclient_mock):
assert len(hass.states.async_all()) == 0
-async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket):
+@pytest.mark.parametrize(
+ "input,expected",
+ [
+ ( # RGB light in color temp color mode
+ {
+ "colorcapabilities": 31,
+ "ctmax": 500,
+ "ctmin": 153,
+ "etag": "055485a82553e654f156d41c9301b7cf",
+ "hascolor": True,
+ "lastannounced": None,
+ "lastseen": "2021-06-10T20:25Z",
+ "manufacturername": "Philips",
+ "modelid": "LLC020",
+ "name": "Hue Go",
+ "state": {
+ "alert": "none",
+ "bri": 254,
+ "colormode": "ct",
+ "ct": 375,
+ "effect": "none",
+ "hue": 8348,
+ "on": True,
+ "reachable": True,
+ "sat": 147,
+ "xy": [0.462, 0.4111],
+ },
+ "swversion": "5.127.1.26420",
+ "type": "Extended color light",
+ "uniqueid": "00:17:88:01:01:23:45:67-00",
+ },
+ {
+ "entity_id": "light.hue_go",
+ "state": STATE_ON,
+ "attributes": {
+ ATTR_BRIGHTNESS: 254,
+ ATTR_COLOR_TEMP: 375,
+ ATTR_EFFECT_LIST: [EFFECT_COLORLOOP],
+ ATTR_SUPPORTED_COLOR_MODES: [
+ COLOR_MODE_COLOR_TEMP,
+ COLOR_MODE_HS,
+ COLOR_MODE_XY,
+ ],
+ ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP,
+ ATTR_MIN_MIREDS: 153,
+ ATTR_MAX_MIREDS: 500,
+ ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION
+ | SUPPORT_FLASH
+ | SUPPORT_EFFECT,
+ DECONZ_GROUP: False,
+ },
+ },
+ ),
+ ( # RGB light in XY color mode
+ {
+ "colorcapabilities": 0,
+ "ctmax": 65535,
+ "ctmin": 0,
+ "etag": "74c91da78bbb5f4dc4d36edf4ad6857c",
+ "hascolor": True,
+ "lastannounced": "2021-01-27T18:05:38Z",
+ "lastseen": "2021-06-10T20:26Z",
+ "manufacturername": "Philips",
+ "modelid": "4090331P9_01",
+ "name": "Hue Ensis",
+ "state": {
+ "alert": "none",
+ "bri": 254,
+ "colormode": "xy",
+ "ct": 316,
+ "effect": "0",
+ "hue": 3096,
+ "on": True,
+ "reachable": True,
+ "sat": 48,
+ "xy": [0.427, 0.373],
+ },
+ "swversion": "1.65.9_hB3217DF4",
+ "type": "Extended color light",
+ "uniqueid": "00:17:88:01:01:23:45:67-01",
+ },
+ {
+ "entity_id": "light.hue_ensis",
+ "state": STATE_ON,
+ "attributes": {
+ ATTR_MIN_MIREDS: 140,
+ ATTR_MAX_MIREDS: 650,
+ ATTR_EFFECT_LIST: [EFFECT_COLORLOOP],
+ ATTR_SUPPORTED_COLOR_MODES: [
+ COLOR_MODE_COLOR_TEMP,
+ COLOR_MODE_HS,
+ COLOR_MODE_XY,
+ ],
+ ATTR_COLOR_MODE: COLOR_MODE_XY,
+ ATTR_BRIGHTNESS: 254,
+ ATTR_HS_COLOR: (29.691, 38.039),
+ ATTR_RGB_COLOR: (255, 206, 158),
+ ATTR_XY_COLOR: (0.427, 0.373),
+ DECONZ_GROUP: False,
+ ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION
+ | SUPPORT_FLASH
+ | SUPPORT_EFFECT,
+ },
+ },
+ ),
+ ( # RGB light with only HS color mode
+ {
+ "etag": "87a89542bf9b9d0aa8134919056844f8",
+ "hascolor": True,
+ "lastannounced": None,
+ "lastseen": "2020-12-05T22:57Z",
+ "manufacturername": "_TZE200_s8gkrkxk",
+ "modelid": "TS0601",
+ "name": "LIDL xmas light",
+ "state": {
+ "bri": 25,
+ "colormode": "hs",
+ "effect": "none",
+ "hue": 53691,
+ "on": True,
+ "reachable": True,
+ "sat": 141,
+ },
+ "swversion": None,
+ "type": "Color dimmable light",
+ "uniqueid": "58:8e:81:ff:fe:db:7b:be-01",
+ },
+ {
+ "entity_id": "light.lidl_xmas_light",
+ "state": STATE_ON,
+ "attributes": {
+ ATTR_EFFECT_LIST: [EFFECT_COLORLOOP],
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS],
+ ATTR_COLOR_MODE: COLOR_MODE_HS,
+ ATTR_BRIGHTNESS: 25,
+ ATTR_HS_COLOR: (294.938, 55.294),
+ ATTR_RGB_COLOR: (243, 113, 255),
+ ATTR_XY_COLOR: (0.357, 0.188),
+ DECONZ_GROUP: False,
+ ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION
+ | SUPPORT_FLASH
+ | SUPPORT_EFFECT,
+ },
+ },
+ ),
+ ( # Tunable white light in CT color mode
+ {
+ "colorcapabilities": 16,
+ "ctmax": 454,
+ "ctmin": 153,
+ "etag": "576ffecbedb4abdc3d3f375fd8f17a9e",
+ "hascolor": True,
+ "lastannounced": None,
+ "lastseen": "2021-06-10T20:25Z",
+ "manufacturername": "Philips",
+ "modelid": "LTW013",
+ "name": "Hue White Ambiance",
+ "state": {
+ "alert": "none",
+ "bri": 254,
+ "colormode": "ct",
+ "ct": 396,
+ "on": True,
+ "reachable": True,
+ },
+ "swversion": "1.46.13_r26312",
+ "type": "Color temperature light",
+ "uniqueid": "00:17:88:01:01:23:45:67-02",
+ },
+ {
+ "entity_id": "light.hue_white_ambiance",
+ "state": STATE_ON,
+ "attributes": {
+ ATTR_MIN_MIREDS: 153,
+ ATTR_MAX_MIREDS: 454,
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_COLOR_TEMP],
+ ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP,
+ ATTR_BRIGHTNESS: 254,
+ ATTR_COLOR_TEMP: 396,
+ DECONZ_GROUP: False,
+ ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION | SUPPORT_FLASH,
+ },
+ },
+ ),
+ ( # Dimmable light
+ {
+ "etag": "f88e87235e2abce62404edd99b1af323",
+ "hascolor": False,
+ "lastannounced": None,
+ "lastseen": "2021-06-10T20:26Z",
+ "manufacturername": "Philips",
+ "modelid": "LWO001",
+ "name": "Hue Filament",
+ "state": {"alert": "none", "bri": 254, "on": True, "reachable": True},
+ "swversion": "1.55.8_r28815",
+ "type": "Dimmable light",
+ "uniqueid": "00:17:88:01:01:23:45:67-03",
+ },
+ {
+ "entity_id": "light.hue_filament",
+ "state": STATE_ON,
+ "attributes": {
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_BRIGHTNESS],
+ ATTR_COLOR_MODE: COLOR_MODE_BRIGHTNESS,
+ ATTR_BRIGHTNESS: 254,
+ DECONZ_GROUP: False,
+ ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION | SUPPORT_FLASH,
+ },
+ },
+ ),
+ ( # On/Off light
+ {
+ "etag": "99c67fd8f0529c6c2aab94b45e4f6caa",
+ "hascolor": False,
+ "lastannounced": "2021-04-26T20:28:11Z",
+ "lastseen": "2021-06-10T21:15Z",
+ "manufacturername": "Unknown",
+ "modelid": "Unknown",
+ "name": "Simple Light",
+ "state": {"alert": "none", "on": True, "reachable": True},
+ "swversion": "2.0",
+ "type": "Simple light",
+ "uniqueid": "00:15:8d:00:01:23:45:67-01",
+ },
+ {
+ "entity_id": "light.simple_light",
+ "state": STATE_ON,
+ "attributes": {
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_ONOFF],
+ ATTR_COLOR_MODE: COLOR_MODE_ONOFF,
+ DECONZ_GROUP: False,
+ ATTR_SUPPORTED_FEATURES: 0,
+ },
+ },
+ ),
+ ],
+)
+async def test_lights(hass, aioclient_mock, input, expected):
+ """Test that different light entities are created with expected values."""
+ data = {"lights": {"0": input}}
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ assert len(hass.states.async_all()) == 1
+
+ light = hass.states.get(expected["entity_id"])
+ assert light.state == expected["state"]
+ for attribute, expected_value in expected["attributes"].items():
+ assert light.attributes[attribute] == expected_value
+
+ await hass.config_entries.async_unload(config_entry.entry_id)
+
+ states = hass.states.async_all()
+ for state in states:
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
+
+
+async def test_light_state_change(hass, aioclient_mock, mock_deconz_websocket):
+ """Verify light can change state on websocket event."""
+ data = {
+ "lights": {
+ "0": {
+ "colorcapabilities": 31,
+ "ctmax": 500,
+ "ctmin": 153,
+ "etag": "055485a82553e654f156d41c9301b7cf",
+ "hascolor": True,
+ "lastannounced": None,
+ "lastseen": "2021-06-10T20:25Z",
+ "manufacturername": "Philips",
+ "modelid": "LLC020",
+ "name": "Hue Go",
+ "state": {
+ "alert": "none",
+ "bri": 254,
+ "colormode": "ct",
+ "ct": 375,
+ "effect": "none",
+ "hue": 8348,
+ "on": True,
+ "reachable": True,
+ "sat": 147,
+ "xy": [0.462, 0.4111],
+ },
+ "swversion": "5.127.1.26420",
+ "type": "Extended color light",
+ "uniqueid": "00:17:88:01:01:23:45:67-00",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
+
+ assert hass.states.get("light.hue_go").state == STATE_ON
+
+ event_changed_light = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "0",
+ "state": {"on": False},
+ }
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("light.hue_go").state == STATE_OFF
+
+
+@pytest.mark.parametrize(
+ "input,expected",
+ [
+ ( # Turn on light with short color loop
+ {
+ "light_on": False,
+ "service": SERVICE_TURN_ON,
+ "call": {
+ ATTR_ENTITY_ID: "light.hue_go",
+ ATTR_BRIGHTNESS: 200,
+ ATTR_COLOR_TEMP: 200,
+ ATTR_TRANSITION: 5,
+ ATTR_FLASH: FLASH_SHORT,
+ ATTR_EFFECT: EFFECT_COLORLOOP,
+ },
+ },
+ {
+ "bri": 200,
+ "ct": 200,
+ "transitiontime": 50,
+ "alert": "select",
+ "effect": "colorloop",
+ },
+ ),
+ ( # Turn on light disabling color loop with long flashing
+ {
+ "light_on": False,
+ "service": SERVICE_TURN_ON,
+ "call": {
+ ATTR_ENTITY_ID: "light.hue_go",
+ ATTR_XY_COLOR: (0.411, 0.351),
+ ATTR_FLASH: FLASH_LONG,
+ ATTR_EFFECT: "None",
+ },
+ },
+ {
+ "xy": (0.411, 0.351),
+ "alert": "lselect",
+ "effect": "none",
+ },
+ ),
+ ( # Turn off light with short flashing
+ {
+ "light_on": True,
+ "service": SERVICE_TURN_OFF,
+ "call": {
+ ATTR_ENTITY_ID: "light.hue_go",
+ ATTR_TRANSITION: 5,
+ ATTR_FLASH: FLASH_SHORT,
+ },
+ },
+ {
+ "bri": 0,
+ "transitiontime": 50,
+ "alert": "select",
+ },
+ ),
+ ( # Turn off light with long flashing
+ {
+ "light_on": True,
+ "service": SERVICE_TURN_OFF,
+ "call": {ATTR_ENTITY_ID: "light.hue_go", ATTR_FLASH: FLASH_LONG},
+ },
+ {"alert": "lselect"},
+ ),
+ ( # Turn off light when light is already off is not supported
+ {
+ "light_on": False,
+ "service": SERVICE_TURN_OFF,
+ "call": {
+ ATTR_ENTITY_ID: "light.hue_go",
+ ATTR_TRANSITION: 5,
+ ATTR_FLASH: FLASH_SHORT,
+ },
+ },
+ {},
+ ),
+ ],
+)
+async def test_light_service_calls(hass, aioclient_mock, input, expected):
+ """Verify light can change state on websocket event."""
+ data = {
+ "lights": {
+ "0": {
+ "colorcapabilities": 31,
+ "ctmax": 500,
+ "ctmin": 153,
+ "etag": "055485a82553e654f156d41c9301b7cf",
+ "hascolor": True,
+ "lastannounced": None,
+ "lastseen": "2021-06-10T20:25Z",
+ "manufacturername": "Philips",
+ "modelid": "LLC020",
+ "name": "Hue Go",
+ "state": {
+ "alert": "none",
+ "bri": 254,
+ "colormode": "ct",
+ "ct": 375,
+ "effect": "none",
+ "hue": 8348,
+ "on": input["light_on"],
+ "reachable": True,
+ "sat": 147,
+ "xy": [0.462, 0.4111],
+ },
+ "swversion": "5.127.1.26420",
+ "type": "Extended color light",
+ "uniqueid": "00:17:88:01:01:23:45:67-00",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state")
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ input["service"],
+ input["call"],
+ blocking=True,
+ )
+ if expected:
+ assert aioclient_mock.mock_calls[1][2] == expected
+ else:
+ assert len(aioclient_mock.mock_calls) == 1 # not called
+
+
+async def test_ikea_default_transition_time(hass, aioclient_mock):
+ """Verify that service calls to IKEA lights always extend with transition tinme 0 if absent."""
+ data = {
+ "lights": {
+ "0": {
+ "colorcapabilities": 0,
+ "ctmax": 65535,
+ "ctmin": 0,
+ "etag": "9dd510cd474791481f189d2a68a3c7f1",
+ "hascolor": True,
+ "lastannounced": "2020-12-17T17:44:38Z",
+ "lastseen": "2021-01-11T18:36Z",
+ "manufacturername": "IKEA of Sweden",
+ "modelid": "TRADFRI bulb E27 WS opal 1000lm",
+ "name": "IKEA light",
+ "state": {
+ "alert": "none",
+ "bri": 156,
+ "colormode": "ct",
+ "ct": 250,
+ "on": True,
+ "reachable": True,
+ },
+ "swversion": "2.0.022",
+ "type": "Color temperature light",
+ "uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01",
+ },
+ },
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state")
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: "light.ikea_light",
+ ATTR_BRIGHTNESS: 100,
+ },
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {
+ "bri": 100,
+ "on": True,
+ "transitiontime": 0,
+ }
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: "light.ikea_light",
+ ATTR_BRIGHTNESS: 100,
+ ATTR_TRANSITION: 5,
+ },
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {
+ "bri": 100,
+ "on": True,
+ "transitiontime": 50,
+ }
+
+
+async def test_lidl_christmas_light(hass, aioclient_mock):
"""Test that lights or groups entities are created."""
+ data = {
+ "lights": {
+ "0": {
+ "etag": "87a89542bf9b9d0aa8134919056844f8",
+ "hascolor": True,
+ "lastannounced": None,
+ "lastseen": "2020-12-05T22:57Z",
+ "manufacturername": "_TZE200_s8gkrkxk",
+ "modelid": "TS0601",
+ "name": "LIDL xmas light",
+ "state": {
+ "bri": 25,
+ "colormode": "hs",
+ "effect": "none",
+ "hue": 53691,
+ "on": True,
+ "reachable": True,
+ "sat": 141,
+ },
+ "swversion": None,
+ "type": "Color dimmable light",
+ "uniqueid": "58:8e:81:ff:fe:db:7b:be-01",
+ }
+ }
+ }
+
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state")
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: "light.lidl_xmas_light",
+ ATTR_HS_COLOR: (20, 30),
+ },
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"on": True, "hue": 3640, "sat": 76}
+
+ assert hass.states.get("light.lidl_xmas_light")
+
+
+async def test_configuration_tool(hass, aioclient_mock):
+ """Verify that configuration tool is not created."""
+ data = {
+ "lights": {
+ "0": {
+ "etag": "26839cb118f5bf7ba1f2108256644010",
+ "hascolor": False,
+ "lastannounced": None,
+ "lastseen": "2020-11-22T11:27Z",
+ "manufacturername": "dresden elektronik",
+ "modelid": "ConBee II",
+ "name": "Configuration tool 1",
+ "state": {"reachable": True},
+ "swversion": "0x264a0700",
+ "type": "Configuration tool",
+ "uniqueid": "00:21:2e:ff:ff:05:a7:a3-01",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
+
+ assert len(hass.states.async_all()) == 0
+
+
+@pytest.mark.parametrize(
+ "input,expected",
+ [
+ (
+ {
+ "lights": ["1", "2", "3"],
+ },
+ {
+ "entity_id": "light.group",
+ "state": ATTR_ON,
+ "attributes": {
+ ATTR_MIN_MIREDS: 153,
+ ATTR_MAX_MIREDS: 500,
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_COLOR_TEMP, COLOR_MODE_XY],
+ ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP,
+ ATTR_BRIGHTNESS: 255,
+ ATTR_EFFECT_LIST: [EFFECT_COLORLOOP],
+ "all_on": False,
+ DECONZ_GROUP: True,
+ ATTR_SUPPORTED_FEATURES: 44,
+ },
+ },
+ ),
+ (
+ {
+ "lights": ["3", "1", "2"],
+ },
+ {
+ "entity_id": "light.group",
+ "state": ATTR_ON,
+ "attributes": {
+ ATTR_MIN_MIREDS: 153,
+ ATTR_MAX_MIREDS: 500,
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_COLOR_TEMP, COLOR_MODE_XY],
+ ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP,
+ ATTR_BRIGHTNESS: 50,
+ ATTR_EFFECT_LIST: [EFFECT_COLORLOOP],
+ "all_on": False,
+ DECONZ_GROUP: True,
+ ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION
+ | SUPPORT_FLASH
+ | SUPPORT_EFFECT,
+ },
+ },
+ ),
+ (
+ {
+ "lights": ["2", "3", "1"],
+ },
+ {
+ "entity_id": "light.group",
+ "state": ATTR_ON,
+ "attributes": {
+ ATTR_MIN_MIREDS: 153,
+ ATTR_MAX_MIREDS: 500,
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_COLOR_TEMP, COLOR_MODE_XY],
+ ATTR_COLOR_MODE: COLOR_MODE_XY,
+ ATTR_HS_COLOR: (52.0, 100.0),
+ ATTR_RGB_COLOR: (255, 221, 0),
+ ATTR_XY_COLOR: (0.5, 0.5),
+ "all_on": False,
+ DECONZ_GROUP: True,
+ ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION
+ | SUPPORT_FLASH
+ | SUPPORT_EFFECT,
+ },
+ },
+ ),
+ ],
+)
+async def test_groups(hass, aioclient_mock, input, expected):
+ """Test that different group entities are created with expected values."""
data = {
"groups": {
- "1": {
+ "0": {
"id": "Light group id",
- "name": "Light group",
+ "name": "Group",
"type": "LightGroup",
"state": {"all_on": False, "any_on": True},
"action": {},
"scenes": [],
- "lights": ["1", "2"],
- },
- "2": {
- "id": "Empty group id",
- "name": "Empty group",
- "type": "LightGroup",
- "state": {},
- "action": {},
- "scenes": [],
- "lights": [],
+ "lights": input["lights"],
},
},
"lights": {
@@ -70,10 +724,10 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket):
"name": "RGB light",
"state": {
"on": True,
- "bri": 255,
+ "bri": 50,
"colormode": "xy",
"effect": "colorloop",
- "xy": (500, 500),
+ "xy": (0.5, 0.5),
"reachable": True,
},
"type": "Extended color light",
@@ -83,183 +737,36 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket):
"ctmax": 454,
"ctmin": 155,
"name": "Tunable white light",
- "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True},
+ "state": {
+ "on": True,
+ "colormode": "ct",
+ "ct": 2500,
+ "reachable": True,
+ },
"type": "Tunable white light",
"uniqueid": "00:00:00:00:00:00:00:01-00",
},
"3": {
- "name": "On off switch",
- "type": "On/Off plug-in unit",
- "state": {"reachable": True},
+ "name": "Dimmable light",
+ "type": "Dimmable light",
+ "state": {"bri": 255, "on": True, "reachable": True},
"uniqueid": "00:00:00:00:00:00:00:02-00",
},
- "4": {
- "name": "On off light",
- "state": {"on": True, "reachable": True},
- "type": "On and Off light",
- "uniqueid": "00:00:00:00:00:00:00:03-00",
- },
- "5": {
- "ctmax": 1000,
- "ctmin": 0,
- "name": "Tunable white light with bad maxmin values",
- "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True},
- "type": "Tunable white light",
- "uniqueid": "00:00:00:00:00:00:00:04-00",
- },
},
}
with patch.dict(DECONZ_WEB_REQUEST, data):
config_entry = await setup_deconz_integration(hass, aioclient_mock)
- assert len(hass.states.async_all()) == 6
+ assert len(hass.states.async_all()) == 4
- rgb_light = hass.states.get("light.rgb_light")
- assert rgb_light.state == STATE_ON
- assert rgb_light.attributes[ATTR_BRIGHTNESS] == 255
- assert rgb_light.attributes[ATTR_HS_COLOR] == (224.235, 100.0)
- assert rgb_light.attributes["is_deconz_group"] is False
- assert rgb_light.attributes[ATTR_SUPPORTED_FEATURES] == 61
-
- tunable_white_light = hass.states.get("light.tunable_white_light")
- assert tunable_white_light.state == STATE_ON
- assert tunable_white_light.attributes[ATTR_COLOR_TEMP] == 2500
- assert tunable_white_light.attributes[ATTR_MAX_MIREDS] == 454
- assert tunable_white_light.attributes[ATTR_MIN_MIREDS] == 155
- assert tunable_white_light.attributes[ATTR_SUPPORTED_FEATURES] == 2
-
- tunable_white_light_bad_maxmin = hass.states.get(
- "light.tunable_white_light_with_bad_maxmin_values"
- )
- assert tunable_white_light_bad_maxmin.state == STATE_ON
- assert tunable_white_light_bad_maxmin.attributes[ATTR_COLOR_TEMP] == 2500
- assert tunable_white_light_bad_maxmin.attributes[ATTR_MAX_MIREDS] == 650
- assert tunable_white_light_bad_maxmin.attributes[ATTR_MIN_MIREDS] == 140
- assert tunable_white_light_bad_maxmin.attributes[ATTR_SUPPORTED_FEATURES] == 2
-
- on_off_light = hass.states.get("light.on_off_light")
- assert on_off_light.state == STATE_ON
- assert on_off_light.attributes[ATTR_SUPPORTED_FEATURES] == 0
-
- assert hass.states.get("light.light_group").state == STATE_ON
- assert hass.states.get("light.light_group").attributes["all_on"] is False
-
- empty_group = hass.states.get("light.empty_group")
- assert empty_group is None
-
- event_changed_light = {
- "t": "event",
- "e": "changed",
- "r": "lights",
- "id": "1",
- "state": {"on": False},
- }
- await mock_deconz_websocket(data=event_changed_light)
- await hass.async_block_till_done()
-
- assert hass.states.get("light.rgb_light").state == STATE_OFF
-
- # Verify service calls
-
- mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
-
- # Service turn on light with short color loop
-
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_ENTITY_ID: "light.rgb_light",
- ATTR_COLOR_TEMP: 2500,
- ATTR_BRIGHTNESS: 200,
- ATTR_TRANSITION: 5,
- ATTR_FLASH: FLASH_SHORT,
- ATTR_EFFECT: EFFECT_COLORLOOP,
- },
- blocking=True,
- )
- assert aioclient_mock.mock_calls[1][2] == {
- "bri": 200,
- "transitiontime": 50,
- "alert": "select",
- "effect": "colorloop",
- }
-
- # Service turn on light disabling color loop with long flashing
-
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_ENTITY_ID: "light.rgb_light",
- ATTR_HS_COLOR: (20, 30),
- ATTR_FLASH: FLASH_LONG,
- ATTR_EFFECT: "None",
- },
- blocking=True,
- )
- assert aioclient_mock.mock_calls[2][2] == {
- "xy": (0.411, 0.351),
- "alert": "lselect",
- "effect": "none",
- }
-
- # Service turn on light with short flashing not supported
-
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {
- ATTR_ENTITY_ID: "light.rgb_light",
- ATTR_TRANSITION: 5,
- ATTR_FLASH: FLASH_SHORT,
- },
- blocking=True,
- )
- assert len(aioclient_mock.mock_calls) == 3 # Not called
-
- event_changed_light = {
- "t": "event",
- "e": "changed",
- "r": "lights",
- "id": "1",
- "state": {"on": True},
- }
- await mock_deconz_websocket(data=event_changed_light)
- await hass.async_block_till_done()
-
- # Service turn off light with short flashing
-
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {
- ATTR_ENTITY_ID: "light.rgb_light",
- ATTR_TRANSITION: 5,
- ATTR_FLASH: FLASH_SHORT,
- },
- blocking=True,
- )
- assert aioclient_mock.mock_calls[3][2] == {
- "bri": 0,
- "transitiontime": 50,
- "alert": "select",
- }
-
- # Service turn off light with long flashing
-
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "light.rgb_light", ATTR_FLASH: FLASH_LONG},
- blocking=True,
- )
- assert aioclient_mock.mock_calls[4][2] == {"alert": "lselect"}
+ group = hass.states.get(expected["entity_id"])
+ assert group.state == expected["state"]
+ for attribute, expected_value in expected["attributes"].items():
+ assert group.attributes[attribute] == expected_value
await hass.config_entries.async_unload(config_entry.entry_id)
states = hass.states.async_all()
- assert len(states) == 6
for state in states:
assert state.state == STATE_UNAVAILABLE
@@ -268,6 +775,155 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket):
assert len(hass.states.async_all()) == 0
+@pytest.mark.parametrize(
+ "input,expected",
+ [
+ ( # Turn on group with short color loop
+ {
+ "lights": ["1", "2", "3"],
+ "group_on": False,
+ "service": SERVICE_TURN_ON,
+ "call": {
+ ATTR_ENTITY_ID: "light.group",
+ ATTR_BRIGHTNESS: 200,
+ ATTR_COLOR_TEMP: 200,
+ ATTR_TRANSITION: 5,
+ ATTR_FLASH: FLASH_SHORT,
+ ATTR_EFFECT: EFFECT_COLORLOOP,
+ },
+ },
+ {
+ "bri": 200,
+ "ct": 200,
+ "transitiontime": 50,
+ "alert": "select",
+ "effect": "colorloop",
+ },
+ ),
+ ( # Turn on group with hs colors
+ {
+ "lights": ["1", "2", "3"],
+ "group_on": False,
+ "service": SERVICE_TURN_ON,
+ "call": {
+ ATTR_ENTITY_ID: "light.group",
+ ATTR_HS_COLOR: (250, 50),
+ },
+ },
+ {
+ "hue": 45510,
+ "on": True,
+ "sat": 127,
+ },
+ ),
+ ( # Turn on group with short color loop
+ {
+ "lights": ["3", "2", "1"],
+ "group_on": False,
+ "service": SERVICE_TURN_ON,
+ "call": {
+ ATTR_ENTITY_ID: "light.group",
+ ATTR_HS_COLOR: (250, 50),
+ },
+ },
+ {
+ "hue": 45510,
+ "on": True,
+ "sat": 127,
+ },
+ ),
+ ],
+)
+async def test_group_service_calls(hass, aioclient_mock, input, expected):
+ """Verify expected group web request from different service calls."""
+ data = {
+ "groups": {
+ "0": {
+ "id": "Light group id",
+ "name": "Group",
+ "type": "LightGroup",
+ "state": {"all_on": False, "any_on": input["group_on"]},
+ "action": {},
+ "scenes": [],
+ "lights": input["lights"],
+ },
+ },
+ "lights": {
+ "1": {
+ "name": "RGB light",
+ "state": {
+ "bri": 255,
+ "colormode": "xy",
+ "effect": "colorloop",
+ "hue": 53691,
+ "on": True,
+ "reachable": True,
+ "sat": 141,
+ "xy": (0.5, 0.5),
+ },
+ "type": "Extended color light",
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
+ "2": {
+ "ctmax": 454,
+ "ctmin": 155,
+ "name": "Tunable white light",
+ "state": {
+ "on": True,
+ "colormode": "ct",
+ "ct": 2500,
+ "reachable": True,
+ },
+ "type": "Tunable white light",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ "3": {
+ "name": "Dimmable light",
+ "type": "Dimmable light",
+ "state": {"bri": 254, "on": True, "reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
+ },
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/groups/0/action")
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ input["service"],
+ input["call"],
+ blocking=True,
+ )
+ if expected:
+ assert aioclient_mock.mock_calls[1][2] == expected
+ else:
+ assert len(aioclient_mock.mock_calls) == 1 # not called
+
+
+async def test_empty_group(hass, aioclient_mock):
+ """Verify that a group without a list of lights is not created."""
+ data = {
+ "groups": {
+ "0": {
+ "id": "Empty group id",
+ "name": "Empty group",
+ "type": "LightGroup",
+ "state": {},
+ "action": {},
+ "scenes": [],
+ "lights": [],
+ },
+ },
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
+
+ assert len(hass.states.async_all()) == 0
+ assert not hass.states.get("light.empty_group")
+
+
async def test_disable_light_groups(hass, aioclient_mock):
"""Test disallowing light groups work."""
data = {
@@ -331,109 +987,6 @@ async def test_disable_light_groups(hass, aioclient_mock):
assert not hass.states.get("light.light_group")
-async def test_configuration_tool(hass, aioclient_mock):
- """Test that configuration tool is not created."""
- data = {
- "lights": {
- "0": {
- "etag": "26839cb118f5bf7ba1f2108256644010",
- "hascolor": False,
- "lastannounced": None,
- "lastseen": "2020-11-22T11:27Z",
- "manufacturername": "dresden elektronik",
- "modelid": "ConBee II",
- "name": "Configuration tool 1",
- "state": {"reachable": True},
- "swversion": "0x264a0700",
- "type": "Configuration tool",
- "uniqueid": "00:21:2e:ff:ff:05:a7:a3-01",
- }
- }
- }
- with patch.dict(DECONZ_WEB_REQUEST, data):
- await setup_deconz_integration(hass, aioclient_mock)
-
- assert len(hass.states.async_all()) == 0
-
-
-async def test_ikea_default_transition_time(hass, aioclient_mock):
- """Verify that service calls to IKEA lights always extend with transition tinme 0 if absent."""
- data = {
- "lights": {
- "1": {
- "manufacturername": "IKEA",
- "name": "Dimmable light",
- "state": {"on": True, "bri": 255, "reachable": True},
- "type": "Dimmable light",
- "uniqueid": "00:00:00:00:00:00:00:01-00",
- },
- },
- }
- with patch.dict(DECONZ_WEB_REQUEST, data):
- config_entry = await setup_deconz_integration(hass, aioclient_mock)
-
- mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
-
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "light.dimmable_light", ATTR_BRIGHTNESS: 100},
- blocking=True,
- )
- assert aioclient_mock.mock_calls[1][2] == {
- "bri": 100,
- "on": True,
- "transitiontime": 0,
- }
-
-
-async def test_lidl_christmas_light(hass, aioclient_mock):
- """Test that lights or groups entities are created."""
- data = {
- "lights": {
- "0": {
- "etag": "87a89542bf9b9d0aa8134919056844f8",
- "hascolor": True,
- "lastannounced": None,
- "lastseen": "2020-12-05T22:57Z",
- "manufacturername": "_TZE200_s8gkrkxk",
- "modelid": "TS0601",
- "name": "xmas light",
- "state": {
- "bri": 25,
- "colormode": "hs",
- "effect": "none",
- "hue": 53691,
- "on": True,
- "reachable": True,
- "sat": 141,
- },
- "swversion": None,
- "type": "Color dimmable light",
- "uniqueid": "58:8e:81:ff:fe:db:7b:be-01",
- }
- }
- }
-
- with patch.dict(DECONZ_WEB_REQUEST, data):
- config_entry = await setup_deconz_integration(hass, aioclient_mock)
-
- mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state")
-
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_ENTITY_ID: "light.xmas_light",
- ATTR_HS_COLOR: (20, 30),
- },
- blocking=True,
- )
- assert aioclient_mock.mock_calls[1][2] == {"on": True, "hue": 3640, "sat": 76}
-
- assert hass.states.get("light.xmas_light")
-
-
async def test_non_color_light_reports_color(
hass, aioclient_mock, mock_deconz_websocket
):
@@ -460,7 +1013,7 @@ async def test_non_color_light_reports_color(
"devicemembership": [],
"etag": "81e42cf1b47affb72fa72bc2e25ba8bf",
"lights": ["0", "1"],
- "name": "All",
+ "name": "Group",
"scenes": [],
"state": {"all_on": False, "any_on": True},
"type": "LightGroup",
@@ -522,7 +1075,7 @@ async def test_non_color_light_reports_color(
await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 3
- assert hass.states.get("light.all").attributes[ATTR_COLOR_TEMP] == 307
+ assert hass.states.get("light.group").attributes[ATTR_COLOR_TEMP] == 250
# Updating a scene will return a faulty color value for a non-color light causing an exception in hs_color
event_changed_light = {
@@ -545,8 +1098,8 @@ async def test_non_color_light_reports_color(
# Bug is fixed if we reach this point, but device won't have neither color temp nor color
with pytest.raises(KeyError):
- assert hass.states.get("light.all").attributes[ATTR_COLOR_TEMP]
- assert hass.states.get("light.all").attributes[ATTR_HS_COLOR]
+ assert hass.states.get("light.group").attributes[ATTR_COLOR_TEMP]
+ assert hass.states.get("light.group").attributes[ATTR_HS_COLOR]
async def test_verify_group_supported_features(hass, aioclient_mock):
@@ -555,7 +1108,7 @@ async def test_verify_group_supported_features(hass, aioclient_mock):
"groups": {
"1": {
"id": "Group1",
- "name": "group",
+ "name": "Group",
"type": "LightGroup",
"state": {"all_on": False, "any_on": True},
"action": {},
@@ -599,4 +1152,7 @@ async def test_verify_group_supported_features(hass, aioclient_mock):
assert len(hass.states.async_all()) == 4
assert hass.states.get("light.group").state == STATE_ON
- assert hass.states.get("light.group").attributes[ATTR_SUPPORTED_FEATURES] == 63
+ assert (
+ hass.states.get("light.group").attributes[ATTR_SUPPORTED_FEATURES]
+ == SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_EFFECT
+ )
diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py
index 7ad9c82b08c..8a696da9eb4 100644
--- a/tests/components/deconz/test_services.py
+++ b/tests/components/deconz/test_services.py
@@ -152,8 +152,27 @@ async def test_configure_service_with_entity_and_field(hass, aioclient_mock):
assert aioclient_mock.mock_calls[1][2] == {"on": True, "attr1": 10, "attr2": 20}
+async def test_configure_service_with_faulty_bridgeid(hass, aioclient_mock):
+ """Test that service fails on a bad bridge id."""
+ await setup_deconz_integration(hass, aioclient_mock)
+ aioclient_mock.clear_requests()
+
+ data = {
+ CONF_BRIDGE_ID: "Bad bridge id",
+ SERVICE_FIELD: "/lights/1",
+ SERVICE_DATA: {"on": True},
+ }
+
+ await hass.services.async_call(
+ DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data
+ )
+ await hass.async_block_till_done()
+
+ assert len(aioclient_mock.mock_calls) == 0
+
+
async def test_configure_service_with_faulty_field(hass, aioclient_mock):
- """Test that service invokes pydeconz with the correct path and data."""
+ """Test that service fails on a bad field."""
await setup_deconz_integration(hass, aioclient_mock)
data = {SERVICE_FIELD: "light/2", SERVICE_DATA: {}}
@@ -166,7 +185,7 @@ async def test_configure_service_with_faulty_field(hass, aioclient_mock):
async def test_configure_service_with_faulty_entity(hass, aioclient_mock):
- """Test that service invokes pydeconz with the correct path and data."""
+ """Test that service on a non existing entity."""
await setup_deconz_integration(hass, aioclient_mock)
aioclient_mock.clear_requests()
diff --git a/tests/components/demo/test_select.py b/tests/components/demo/test_select.py
new file mode 100644
index 00000000000..628c173da7e
--- /dev/null
+++ b/tests/components/demo/test_select.py
@@ -0,0 +1,73 @@
+"""The tests for the demo select component."""
+
+import pytest
+
+from homeassistant.components.select.const import (
+ ATTR_OPTION,
+ ATTR_OPTIONS,
+ DOMAIN,
+ SERVICE_SELECT_OPTION,
+)
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import HomeAssistant
+from homeassistant.setup import async_setup_component
+
+ENTITY_SPEED = "select.speed"
+
+
+@pytest.fixture(autouse=True)
+async def setup_demo_select(hass: HomeAssistant) -> None:
+ """Initialize setup demo select entity."""
+ assert await async_setup_component(hass, DOMAIN, {"select": {"platform": "demo"}})
+ await hass.async_block_till_done()
+
+
+def test_setup_params(hass: HomeAssistant) -> None:
+ """Test the initial parameters."""
+ state = hass.states.get(ENTITY_SPEED)
+ assert state
+ assert state.state == "ridiculous_speed"
+ assert state.attributes.get(ATTR_OPTIONS) == [
+ "light_speed",
+ "ridiculous_speed",
+ "ludicrous_speed",
+ ]
+
+
+async def test_select_option_bad_attr(hass: HomeAssistant) -> None:
+ """Test selecting a different option with invalid option value."""
+ state = hass.states.get(ENTITY_SPEED)
+ assert state
+ assert state.state == "ridiculous_speed"
+
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {ATTR_OPTION: "slow_speed", ATTR_ENTITY_ID: ENTITY_SPEED},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_SPEED)
+ assert state
+ assert state.state == "ridiculous_speed"
+
+
+async def test_select_option(hass: HomeAssistant) -> None:
+ """Test selecting of a option."""
+ state = hass.states.get(ENTITY_SPEED)
+ assert state
+ assert state.state == "ridiculous_speed"
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {ATTR_OPTION: "light_speed", ATTR_ENTITY_ID: ENTITY_SPEED},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_SPEED)
+ assert state
+ assert state.state == "light_speed"
diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py
new file mode 100644
index 00000000000..d2ba69d9440
--- /dev/null
+++ b/tests/components/devolo_home_control/mocks.py
@@ -0,0 +1,91 @@
+"""Mocks for tests."""
+
+from unittest.mock import MagicMock
+
+from devolo_home_control_api.publisher.publisher import Publisher
+
+
+class BinarySensorPropertyMock:
+ """devolo Home Control binary sensor mock."""
+
+ element_uid = "Test"
+ key_count = 1
+ sensor_type = "door"
+ sub_type = ""
+ state = False
+
+
+class SettingsMock:
+ """devolo Home Control settings mock."""
+
+ name = "Test"
+ zone = "Test"
+
+
+class DeviceMock:
+ """devolo Home Control device mock."""
+
+ available = True
+ brand = "devolo"
+ name = "Test Device"
+ uid = "Test"
+ settings_property = {"general_device_settings": SettingsMock()}
+
+ def is_online(self):
+ """Mock online state of the device."""
+ return DeviceMock.available
+
+
+class BinarySensorMock(DeviceMock):
+ """devolo Home Control binary sensor device mock."""
+
+ binary_sensor_property = {"Test": BinarySensorPropertyMock()}
+
+
+class RemoteControlMock(DeviceMock):
+ """devolo Home Control remote control device mock."""
+
+ remote_control_property = {"Test": BinarySensorPropertyMock()}
+
+
+class DisabledBinarySensorMock(DeviceMock):
+ """devolo Home Control disabled binary sensor device mock."""
+
+ binary_sensor_property = {"devolo.WarningBinaryFI:Test": BinarySensorPropertyMock()}
+
+
+class HomeControlMock:
+ """devolo Home Control gateway mock."""
+
+ binary_sensor_devices = []
+ binary_switch_devices = []
+ multi_level_sensor_devices = []
+ multi_level_switch_devices = []
+ devices = {}
+ publisher = MagicMock()
+
+ def websocket_disconnect(self):
+ """Mock disconnect of the websocket."""
+ pass
+
+
+class HomeControlMockBinarySensor(HomeControlMock):
+ """devolo Home Control gateway mock with binary sensor device."""
+
+ binary_sensor_devices = [BinarySensorMock()]
+ devices = {"Test": BinarySensorMock()}
+ publisher = Publisher(devices.keys())
+ publisher.unregister = MagicMock()
+
+
+class HomeControlMockRemoteControl(HomeControlMock):
+ """devolo Home Control gateway mock with remote control device."""
+
+ devices = {"Test": RemoteControlMock()}
+ publisher = Publisher(devices.keys())
+
+
+class HomeControlMockDisabledBinarySensor(HomeControlMock):
+ """devolo Home Control gateway mock with disabled device."""
+
+ binary_sensor_devices = [DisabledBinarySensorMock()]
diff --git a/tests/components/devolo_home_control/test_binary_sensor.py b/tests/components/devolo_home_control/test_binary_sensor.py
new file mode 100644
index 00000000000..022cd4a1578
--- /dev/null
+++ b/tests/components/devolo_home_control/test_binary_sensor.py
@@ -0,0 +1,112 @@
+"""Tests for the devolo Home Control binary sensors."""
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant.components.binary_sensor import DOMAIN
+from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
+from homeassistant.core import HomeAssistant
+
+from . import configure_integration
+from .mocks import (
+ DeviceMock,
+ HomeControlMock,
+ HomeControlMockBinarySensor,
+ HomeControlMockDisabledBinarySensor,
+ HomeControlMockRemoteControl,
+)
+
+
+@pytest.mark.usefixtures("mock_zeroconf")
+async def test_binary_sensor(hass: HomeAssistant):
+ """Test setup and state change of a binary sensor device."""
+ entry = configure_integration(hass)
+ DeviceMock.available = True
+ with patch(
+ "homeassistant.components.devolo_home_control.HomeControl",
+ side_effect=[HomeControlMockBinarySensor, HomeControlMock],
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(f"{DOMAIN}.test")
+ assert state is not None
+ assert state.state == STATE_OFF
+
+ # Emulate websocket message: sensor turned on
+ HomeControlMockBinarySensor.publisher.dispatch("Test", ("Test", True))
+ await hass.async_block_till_done()
+ assert hass.states.get(f"{DOMAIN}.test").state == STATE_ON
+
+ # Emulate websocket message: device went offline
+ DeviceMock.available = False
+ HomeControlMockBinarySensor.publisher.dispatch("Test", ("Status", False, "status"))
+ await hass.async_block_till_done()
+ assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE
+
+
+@pytest.mark.usefixtures("mock_zeroconf")
+async def test_remote_control(hass: HomeAssistant):
+ """Test setup and state change of a remote control device."""
+ entry = configure_integration(hass)
+ DeviceMock.available = True
+ with patch(
+ "homeassistant.components.devolo_home_control.HomeControl",
+ side_effect=[HomeControlMockRemoteControl, HomeControlMock],
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(f"{DOMAIN}.test")
+ assert state is not None
+ assert state.state == STATE_OFF
+
+ # Emulate websocket message: button pressed
+ HomeControlMockRemoteControl.publisher.dispatch("Test", ("Test", 1))
+ await hass.async_block_till_done()
+ assert hass.states.get(f"{DOMAIN}.test").state == STATE_ON
+
+ # Emulate websocket message: button released
+ HomeControlMockRemoteControl.publisher.dispatch("Test", ("Test", 0))
+ await hass.async_block_till_done()
+ assert hass.states.get(f"{DOMAIN}.test").state == STATE_OFF
+
+ # Emulate websocket message: device went offline
+ DeviceMock.available = False
+ HomeControlMockRemoteControl.publisher.dispatch("Test", ("Status", False, "status"))
+ await hass.async_block_till_done()
+ assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE
+
+
+@pytest.mark.usefixtures("mock_zeroconf")
+async def test_disabled(hass: HomeAssistant):
+ """Test setup of a disabled device."""
+ entry = configure_integration(hass)
+ with patch(
+ "homeassistant.components.devolo_home_control.HomeControl",
+ side_effect=[HomeControlMockDisabledBinarySensor, HomeControlMock],
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert hass.states.get(f"{DOMAIN}.devolo.WarningBinaryFI:Test") is None
+
+
+@pytest.mark.usefixtures("mock_zeroconf")
+async def test_remove_from_hass(hass: HomeAssistant):
+ """Test removing entity."""
+ entry = configure_integration(hass)
+ with patch(
+ "homeassistant.components.devolo_home_control.HomeControl",
+ side_effect=[HomeControlMockBinarySensor, HomeControlMock],
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(f"{DOMAIN}.test")
+ assert state is not None
+ await hass.config_entries.async_remove(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 0
+ HomeControlMockBinarySensor.publisher.unregister.assert_called_once()
diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py
index 94435545cc6..054b613f3a0 100644
--- a/tests/components/devolo_home_control/test_config_flow.py
+++ b/tests/components/devolo_home_control/test_config_flow.py
@@ -5,7 +5,6 @@ import pytest
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.devolo_home_control.const import DEFAULT_MYDEVOLO, DOMAIN
-from homeassistant.config_entries import SOURCE_USER
from .const import (
DISCOVERY_INFO,
@@ -57,7 +56,7 @@ async def test_form_already_configured(hass):
MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
- context={"source": SOURCE_USER},
+ context={"source": config_entries.SOURCE_USER},
data={"username": "test-username", "password": "test-password"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@@ -70,7 +69,7 @@ async def test_form_advanced_options(hass):
DOMAIN,
context={"source": config_entries.SOURCE_USER, "show_advanced_options": True},
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
@@ -157,6 +156,106 @@ async def test_zeroconf_wrong_device(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+async def test_form_reauth(hass):
+ """Test that the reauth confirmation form is served."""
+ mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={})
+ mock_config.add_to_hass(hass)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": config_entries.SOURCE_REAUTH,
+ "entry_id": mock_config.entry_id,
+ },
+ data={
+ "username": "test-username",
+ "password": "test-password",
+ "mydevolo_url": "https://test_mydevolo_url.test",
+ },
+ )
+
+ assert result["step_id"] == "reauth_confirm"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ with patch(
+ "homeassistant.components.devolo_home_control.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry, patch(
+ "homeassistant.components.devolo_home_control.Mydevolo.uuid",
+ return_value="123456",
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"username": "test-username-new", "password": "test-password-new"},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+@pytest.mark.credentials_invalid
+async def test_form_invalid_credentials_reauth(hass):
+ """Test if we get the error message on invalid credentials."""
+ mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={})
+ mock_config.add_to_hass(hass)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": config_entries.SOURCE_REAUTH,
+ "entry_id": mock_config.entry_id,
+ },
+ data={
+ "username": "test-username",
+ "password": "test-password",
+ "mydevolo_url": "https://test_mydevolo_url.test",
+ },
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"username": "test-username", "password": "test-password"},
+ )
+
+ assert result["errors"] == {"base": "invalid_auth"}
+
+
+async def test_form_uuid_change_reauth(hass):
+ """Test that the reauth confirmation form is served."""
+ mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={})
+ mock_config.add_to_hass(hass)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": config_entries.SOURCE_REAUTH,
+ "entry_id": mock_config.entry_id,
+ },
+ data={
+ "username": "test-username",
+ "password": "test-password",
+ "mydevolo_url": "https://test_mydevolo_url.test",
+ },
+ )
+
+ assert result["step_id"] == "reauth_confirm"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ with patch(
+ "homeassistant.components.devolo_home_control.async_setup_entry",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.devolo_home_control.Mydevolo.uuid",
+ return_value="789123",
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"username": "test-username-new", "password": "test-password-new"},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "reauth_failed"}
+
+
async def _setup(hass, result):
"""Finish configuration steps."""
with patch(
diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py
index 4dd77c98187..8be837bb16e 100644
--- a/tests/components/discovery/test_init.py
+++ b/tests/components/discovery/test_init.py
@@ -60,7 +60,7 @@ async def mock_discovery(hass, discoveries, config=BASE_CONFIG):
async def test_unknown_service(hass):
"""Test that unknown service is ignored."""
- def discover(netdisco, zeroconf_instance):
+ def discover(netdisco, zeroconf_instance, suppress_mdns_types):
"""Fake discovery."""
return [("this_service_will_never_be_supported", {"info": "some"})]
@@ -73,7 +73,7 @@ async def test_unknown_service(hass):
async def test_load_platform(hass):
"""Test load a platform."""
- def discover(netdisco, zeroconf_instance):
+ def discover(netdisco, zeroconf_instance, suppress_mdns_types):
"""Fake discovery."""
return [(SERVICE, SERVICE_INFO)]
@@ -89,7 +89,7 @@ async def test_load_platform(hass):
async def test_load_component(hass):
"""Test load a component."""
- def discover(netdisco, zeroconf_instance):
+ def discover(netdisco, zeroconf_instance, suppress_mdns_types):
"""Fake discovery."""
return [(SERVICE_NO_PLATFORM, SERVICE_INFO)]
@@ -109,7 +109,7 @@ async def test_load_component(hass):
async def test_ignore_service(hass):
"""Test ignore service."""
- def discover(netdisco, zeroconf_instance):
+ def discover(netdisco, zeroconf_instance, suppress_mdns_types):
"""Fake discovery."""
return [(SERVICE_NO_PLATFORM, SERVICE_INFO)]
@@ -122,7 +122,7 @@ async def test_ignore_service(hass):
async def test_discover_duplicates(hass):
"""Test load a component."""
- def discover(netdisco, zeroconf_instance):
+ def discover(netdisco, zeroconf_instance, suppress_mdns_types):
"""Fake discovery."""
return [
(SERVICE_NO_PLATFORM, SERVICE_INFO),
@@ -147,7 +147,7 @@ async def test_discover_config_flow(hass):
"""Test discovery triggering a config flow."""
discovery_info = {"hello": "world"}
- def discover(netdisco, zeroconf_instance):
+ def discover(netdisco, zeroconf_instance, suppress_mdns_types):
"""Fake discovery."""
return [("mock-service", discovery_info)]
diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py
index edb3810e24f..006893a81e8 100644
--- a/tests/components/dsmr/test_config_flow.py
+++ b/tests/components/dsmr/test_config_flow.py
@@ -1,18 +1,233 @@
"""Test the DSMR config flow."""
import asyncio
from itertools import chain, repeat
-from unittest.mock import DEFAULT, AsyncMock, patch
+import os
+from unittest.mock import DEFAULT, AsyncMock, MagicMock, patch, sentinel
import serial
+import serial.tools.list_ports
from homeassistant import config_entries, data_entry_flow, setup
-from homeassistant.components.dsmr import DOMAIN
+from homeassistant.components.dsmr import DOMAIN, config_flow
from tests.common import MockConfigEntry
SERIAL_DATA = {"serial_id": "12345678", "serial_id_gas": "123456789"}
+def com_port():
+ """Mock of a serial port."""
+ port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234")
+ port.serial_number = "1234"
+ port.manufacturer = "Virtual serial port"
+ port.device = "/dev/ttyUSB1234"
+ port.description = "Some serial port"
+
+ return port
+
+
+async def test_setup_network(hass, dsmr_connection_send_validate_fixture):
+ """Test we can setup network."""
+ 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"] == {}
+
+ 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"},
+ )
+
+ entry_data = {
+ "host": "10.10.0.1",
+ "port": 1234,
+ "dsmr_version": "2.2",
+ }
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == "10.10.0.1:1234"
+ assert result["data"] == {**entry_data, **SERIAL_DATA}
+
+
+@patch("serial.tools.list_ports.comports", return_value=[com_port()])
+async def test_setup_serial(com_mock, hass, dsmr_connection_send_validate_fixture):
+ """Test we can setup serial."""
+ 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"] == {}
+
+ 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"}
+ )
+
+ entry_data = {
+ "port": port.device,
+ "dsmr_version": "2.2",
+ }
+
+ 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_manual(
+ com_mock, hass, dsmr_connection_send_validate_fixture
+):
+ """Test we can setup serial with manual entry."""
+ 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"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"port": "Enter Manually", "dsmr_version": "2.2"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_serial_manual_path"
+ assert result["errors"] is None
+
+ with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"port": "/dev/ttyUSB0"}
+ )
+
+ entry_data = {
+ "port": "/dev/ttyUSB0",
+ "dsmr_version": "2.2",
+ }
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == "/dev/ttyUSB0"
+ assert result["data"] == {**entry_data, **SERIAL_DATA}
+
+
+@patch("serial.tools.list_ports.comports", return_value=[com_port()])
+async def test_setup_serial_fail(com_mock, hass, dsmr_connection_send_validate_fixture):
+ """Test failed serial connection."""
+ (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ port = com_port()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ # override the mock to have it fail the first time and succeed after
+ first_fail_connection_factory = AsyncMock(
+ return_value=(transport, protocol),
+ side_effect=chain([serial.serialutil.SerialException], repeat(DEFAULT)),
+ )
+
+ 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"] == {}
+
+ with patch(
+ "homeassistant.components.dsmr.config_flow.create_dsmr_reader",
+ first_fail_connection_factory,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"port": port.device, "dsmr_version": "2.2"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_serial"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+@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
+):
+ """Test failed telegram data."""
+ (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ port = com_port()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ protocol.telegram = {}
+
+ 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"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"port": port.device, "dsmr_version": "2.2"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_serial"
+ assert result["errors"] == {"base": "cannot_communicate"}
+
+
async def test_import_usb(hass, dsmr_connection_send_validate_fixture):
"""Test we can import."""
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -265,3 +480,50 @@ async def test_import_luxembourg(hass, dsmr_connection_send_validate_fixture):
assert result["type"] == "create_entry"
assert result["title"] == "/dev/ttyUSB0"
assert result["data"] == {**entry_data, **SERIAL_DATA}
+
+
+def test_get_serial_by_id_no_dir():
+ """Test serial by id conversion if there's no /dev/serial/by-id."""
+ p1 = patch("os.path.isdir", MagicMock(return_value=False))
+ p2 = patch("os.scandir")
+ with p1 as is_dir_mock, p2 as scan_mock:
+ res = config_flow.get_serial_by_id(sentinel.path)
+ assert res is sentinel.path
+ assert is_dir_mock.call_count == 1
+ assert scan_mock.call_count == 0
+
+
+def test_get_serial_by_id():
+ """Test serial by id conversion."""
+ p1 = patch("os.path.isdir", MagicMock(return_value=True))
+ p2 = patch("os.scandir")
+
+ def _realpath(path):
+ if path is sentinel.matched_link:
+ return sentinel.path
+ return sentinel.serial_link_path
+
+ p3 = patch("os.path.realpath", side_effect=_realpath)
+ with p1 as is_dir_mock, p2 as scan_mock, p3:
+ res = config_flow.get_serial_by_id(sentinel.path)
+ assert res is sentinel.path
+ assert is_dir_mock.call_count == 1
+ assert scan_mock.call_count == 1
+
+ entry1 = MagicMock(spec_set=os.DirEntry)
+ entry1.is_symlink.return_value = True
+ entry1.path = sentinel.some_path
+
+ entry2 = MagicMock(spec_set=os.DirEntry)
+ entry2.is_symlink.return_value = False
+ entry2.path = sentinel.other_path
+
+ entry3 = MagicMock(spec_set=os.DirEntry)
+ entry3.is_symlink.return_value = True
+ entry3.path = sentinel.matched_link
+
+ scan_mock.return_value = [entry1, entry2, entry3]
+ res = config_flow.get_serial_by_id(sentinel.path)
+ assert res is sentinel.matched_link
+ assert is_dir_mock.call_count == 2
+ assert scan_mock.call_count == 2
diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py
index 29ab29a0af6..90194eaeb6b 100644
--- a/tests/components/dsmr/test_sensor.py
+++ b/tests/components/dsmr/test_sensor.py
@@ -13,12 +13,21 @@ from unittest.mock import DEFAULT, MagicMock
from homeassistant import config_entries
from homeassistant.components.dsmr.const import DOMAIN
-from homeassistant.components.dsmr.sensor import DerivativeDSMREntity
-from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.sensor import (
+ ATTR_LAST_RESET,
+ ATTR_STATE_CLASS,
+ DOMAIN as SENSOR_DOMAIN,
+ STATE_CLASS_MEASUREMENT,
+)
from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_ICON,
+ ATTR_UNIT_OF_MEASUREMENT,
+ DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_POWER,
ENERGY_KILO_WATT_HOUR,
+ STATE_UNKNOWN,
VOLUME_CUBIC_METERS,
- VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR,
)
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
@@ -123,8 +132,12 @@ async def test_default_setup(hass, dsmr_connection_fixture):
# make sure entities have been created and return 'unknown' state
power_consumption = hass.states.get("sensor.power_consumption")
- assert power_consumption.state == "unknown"
- assert power_consumption.attributes.get("unit_of_measurement") is None
+ assert power_consumption.state == STATE_UNKNOWN
+ assert power_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
+ assert power_consumption.attributes.get(ATTR_ICON) is None
+ assert power_consumption.attributes.get(ATTR_LAST_RESET) is None
+ assert power_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert power_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
# simulate a telegram pushed from the smartmeter and parsed by dsmr_parser
telegram_callback(telegram)
@@ -142,12 +155,22 @@ async def test_default_setup(hass, dsmr_connection_fixture):
# tariff should be translated in human readable and have no unit
power_tariff = hass.states.get("sensor.power_tariff")
assert power_tariff.state == "low"
- assert power_tariff.attributes.get("unit_of_measurement") == ""
+ assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None
+ assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash"
+ assert power_tariff.attributes.get(ATTR_LAST_RESET) is None
+ assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None
+ assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ""
# check if gas consumption is parsed correctly
gas_consumption = hass.states.get("sensor.gas_consumption")
assert gas_consumption.state == "745.695"
- assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS
+ assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None
+ assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire"
+ assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None
+ assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS
+ )
async def test_setup_only_energy(hass, dsmr_connection_fixture):
@@ -179,46 +202,6 @@ async def test_setup_only_energy(hass, dsmr_connection_fixture):
assert not entry
-async def test_derivative():
- """Test calculation of derivative value."""
- from dsmr_parser.objects import MBusObject
-
- config = {"platform": "dsmr"}
-
- entity = DerivativeDSMREntity("test", "test_device", "5678", "1.0.0", config, False)
- await entity.async_update()
-
- assert entity.state is None, "initial state not unknown"
-
- entity.telegram = {
- "1.0.0": MBusObject(
- [
- {"value": datetime.datetime.fromtimestamp(1551642213)},
- {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS},
- ]
- )
- }
- await entity.async_update()
-
- assert entity.state is None, "state after first update should still be unknown"
-
- entity.telegram = {
- "1.0.0": MBusObject(
- [
- {"value": datetime.datetime.fromtimestamp(1551642543)},
- {"value": Decimal(745.698), "unit": VOLUME_CUBIC_METERS},
- ]
- )
- }
- await entity.async_update()
-
- assert (
- abs(entity.state - 0.033) < 0.00001
- ), "state should be hourly usage calculated from first and second update"
-
- assert entity.unit_of_measurement == VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR
-
-
async def test_v4_meter(hass, dsmr_connection_fixture):
"""Test if v4 meter is correctly parsed."""
(connection_factory, transport, protocol) = dsmr_connection_fixture
@@ -271,12 +254,23 @@ async def test_v4_meter(hass, dsmr_connection_fixture):
# tariff should be translated in human readable and have no unit
power_tariff = hass.states.get("sensor.power_tariff")
assert power_tariff.state == "low"
- assert power_tariff.attributes.get("unit_of_measurement") == ""
+ assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None
+ assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash"
+ assert power_tariff.attributes.get(ATTR_LAST_RESET) is None
+ assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None
+ assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ""
# check if gas consumption is parsed correctly
gas_consumption = hass.states.get("sensor.gas_consumption")
assert gas_consumption.state == "745.695"
assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS
+ assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None
+ assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire"
+ assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None
+ assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS
+ )
async def test_v5_meter(hass, dsmr_connection_fixture):
@@ -331,12 +325,22 @@ async def test_v5_meter(hass, dsmr_connection_fixture):
# tariff should be translated in human readable and have no unit
power_tariff = hass.states.get("sensor.power_tariff")
assert power_tariff.state == "low"
- assert power_tariff.attributes.get("unit_of_measurement") == ""
+ assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None
+ assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash"
+ assert power_tariff.attributes.get(ATTR_LAST_RESET) is None
+ assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None
+ assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ""
# check if gas consumption is parsed correctly
gas_consumption = hass.states.get("sensor.gas_consumption")
assert gas_consumption.state == "745.695"
- assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS
+ assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None
+ assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire"
+ assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None
+ assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS
+ )
async def test_luxembourg_meter(hass, dsmr_connection_fixture):
@@ -396,7 +400,13 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture):
power_tariff = hass.states.get("sensor.energy_consumption_total")
assert power_tariff.state == "123.456"
- assert power_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR
+ assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY
+ assert power_tariff.attributes.get(ATTR_ICON) is None
+ assert power_tariff.attributes.get(ATTR_LAST_RESET) is not None
+ assert power_tariff.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
+ )
power_tariff = hass.states.get("sensor.energy_production_total")
assert power_tariff.state == "654.321"
@@ -405,7 +415,13 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture):
# check if gas consumption is parsed correctly
gas_consumption = hass.states.get("sensor.gas_consumption")
assert gas_consumption.state == "745.695"
- assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS
+ assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None
+ assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire"
+ assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None
+ assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS
+ )
async def test_belgian_meter(hass, dsmr_connection_fixture):
@@ -460,12 +476,22 @@ async def test_belgian_meter(hass, dsmr_connection_fixture):
# tariff should be translated in human readable and have no unit
power_tariff = hass.states.get("sensor.power_tariff")
assert power_tariff.state == "normal"
- assert power_tariff.attributes.get("unit_of_measurement") == ""
+ assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None
+ assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash"
+ assert power_tariff.attributes.get(ATTR_LAST_RESET) is None
+ assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None
+ assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ""
# check if gas consumption is parsed correctly
gas_consumption = hass.states.get("sensor.gas_consumption")
assert gas_consumption.state == "745.695"
- assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS
+ assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None
+ assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire"
+ assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None
+ assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS
+ )
async def test_belgian_meter_low(hass, dsmr_connection_fixture):
@@ -509,7 +535,11 @@ async def test_belgian_meter_low(hass, dsmr_connection_fixture):
# tariff should be translated in human readable and have no unit
power_tariff = hass.states.get("sensor.power_tariff")
assert power_tariff.state == "low"
- assert power_tariff.attributes.get("unit_of_measurement") == ""
+ assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None
+ assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash"
+ assert power_tariff.attributes.get(ATTR_LAST_RESET) is None
+ assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None
+ assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ""
async def test_tcp(hass, dsmr_connection_fixture):
diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py
index da6017a71a1..ec466197995 100644
--- a/tests/components/ecobee/test_climate.py
+++ b/tests/components/ecobee/test_climate.py
@@ -13,6 +13,7 @@ def ecobee_fixture():
"""Set up ecobee mock."""
vals = {
"name": "Ecobee",
+ "modelNumber": "athenaSmart",
"program": {
"climates": [
{"name": "Climate1", "climateRef": "c1"},
@@ -21,6 +22,7 @@ def ecobee_fixture():
"currentClimateRef": "c1",
},
"runtime": {
+ "connected": True,
"actualTemperature": 300,
"actualHumidity": 15,
"desiredHeat": 400,
@@ -64,7 +66,8 @@ def data_fixture(ecobee_fixture):
@pytest.fixture(name="thermostat")
def thermostat_fixture(data):
"""Set up ecobee thermostat object."""
- return ecobee.Thermostat(data, 1)
+ thermostat = data.ecobee.get_thermostat(1)
+ return ecobee.Thermostat(data, 1, thermostat)
async def test_name(thermostat):
diff --git a/tests/components/ecobee/test_humidifier.py b/tests/components/ecobee/test_humidifier.py
index dd58decfb32..f8a83e4c905 100644
--- a/tests/components/ecobee/test_humidifier.py
+++ b/tests/components/ecobee/test_humidifier.py
@@ -27,6 +27,7 @@ from homeassistant.const import (
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
+ STATE_ON,
)
from .common import setup_platform
@@ -39,7 +40,7 @@ async def test_attributes(hass):
await setup_platform(hass, HUMIDIFIER_DOMAIN)
state = hass.states.get(DEVICE_ID)
- assert state.state == STATE_OFF
+ assert state.state == STATE_ON
assert state.attributes.get(ATTR_MIN_HUMIDITY) == DEFAULT_MIN_HUMIDITY
assert state.attributes.get(ATTR_MAX_HUMIDITY) == DEFAULT_MAX_HUMIDITY
assert state.attributes.get(ATTR_HUMIDITY) == 40
diff --git a/tests/components/ee_brightbox/test_device_tracker.py b/tests/components/ee_brightbox/test_device_tracker.py
index 06c3ce0cd1d..afe3897eff9 100644
--- a/tests/components/ee_brightbox/test_device_tracker.py
+++ b/tests/components/ee_brightbox/test_device_tracker.py
@@ -2,13 +2,17 @@
from datetime import datetime
from unittest.mock import patch
-from eebrightbox import EEBrightBoxException
+# Integration is disabled
+# from eebrightbox import EEBrightBoxException
import pytest
from homeassistant.components.device_tracker import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM
from homeassistant.setup import async_setup_component
+# Integration is disabled
+pytest.skip("Integration has been disabled in the manifest", allow_module_level=True)
+
def _configure_mock_get_devices(eebrightbox_mock):
eebrightbox_instance = eebrightbox_mock.return_value
@@ -43,7 +47,8 @@ def _configure_mock_get_devices(eebrightbox_mock):
def _configure_mock_failed_config_check(eebrightbox_mock):
eebrightbox_instance = eebrightbox_mock.return_value
- eebrightbox_instance.__enter__.side_effect = EEBrightBoxException(
+ # Integration is disabled
+ eebrightbox_instance.__enter__.side_effect = EEBrightBoxException( # noqa: F821
"Failed to connect to the router"
)
diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py
index cb0b1f39365..f9df29e16ae 100644
--- a/tests/components/emulated_hue/test_hue_api.py
+++ b/tests/components/emulated_hue/test_hue_api.py
@@ -219,6 +219,10 @@ def hue_client(loop, hass_hue, aiohttp_client):
"light.bed_light": {emulated_hue.CONF_ENTITY_HIDDEN: True},
# Kitchen light is explicitly excluded from being exposed
"light.kitchen_lights": {emulated_hue.CONF_ENTITY_HIDDEN: True},
+ # Entrance light is explicitly excluded from being exposed
+ "light.entrance_color_white_lights": {
+ emulated_hue.CONF_ENTITY_HIDDEN: True
+ },
# Ceiling Fan is explicitly excluded from being exposed
"fan.ceiling_fan": {emulated_hue.CONF_ENTITY_HIDDEN: True},
# Expose the script
diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py
index d5968e7f731..735a02e960c 100644
--- a/tests/components/esphome/test_config_flow.py
+++ b/tests/components/esphome/test_config_flow.py
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from homeassistant import config_entries
-from homeassistant.components.esphome import DOMAIN
+from homeassistant.components.esphome import DOMAIN, DomainData
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
@@ -265,7 +265,8 @@ async def test_discovery_already_configured_name(hass, mock_client):
mock_entry_data = MagicMock()
mock_entry_data.device_info.name = "test8266"
- hass.data[DOMAIN] = {entry.entry_id: mock_entry_data}
+ domain_data = DomainData.get(hass)
+ domain_data.set_entry_data(entry, mock_entry_data)
service_info = {
"host": "192.168.43.184",
diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py
index b762f10447f..fe3c271b390 100644
--- a/tests/components/ezviz/test_config_flow.py
+++ b/tests/components/ezviz/test_config_flow.py
@@ -2,8 +2,13 @@
from unittest.mock import patch
-from pyezviz.client import HTTPError, InvalidURL, PyEzvizError
-from pyezviz.test_cam_rtsp import AuthTestResultFailed, InvalidHost
+from pyezviz.exceptions import (
+ AuthTestResultFailed,
+ HTTPError,
+ InvalidHost,
+ InvalidURL,
+ PyEzvizError,
+)
from homeassistant.components.ezviz.const import (
ATTR_SERIAL,
@@ -233,29 +238,28 @@ async def test_async_step_discovery(
async def test_options_flow(hass):
"""Test updating options."""
- with patch("homeassistant.components.ezviz.PLATFORMS", []):
+ with _patch_async_setup_entry() as mock_setup_entry:
entry = await init_integration(hass)
- assert entry.options[CONF_FFMPEG_ARGUMENTS] == DEFAULT_FFMPEG_ARGUMENTS
- assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT
+ assert entry.options[CONF_FFMPEG_ARGUMENTS] == DEFAULT_FFMPEG_ARGUMENTS
+ assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT
- result = await hass.config_entries.options.async_init(entry.entry_id)
- assert result["type"] == RESULT_TYPE_FORM
- assert result["step_id"] == "init"
- assert result["errors"] is None
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+ assert result["errors"] is None
- with _patch_async_setup_entry() as mock_setup_entry:
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_FFMPEG_ARGUMENTS: "/H.264", CONF_TIMEOUT: 25},
)
- await hass.async_block_till_done()
+ await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["data"][CONF_FFMPEG_ARGUMENTS] == "/H.264"
assert result["data"][CONF_TIMEOUT] == 25
- assert len(mock_setup_entry.mock_calls) == 0
+ assert len(mock_setup_entry.mock_calls) == 1
async def test_user_form_exception(hass, ezviz_config_flow):
diff --git a/tests/components/forecast_solar/__init__.py b/tests/components/forecast_solar/__init__.py
new file mode 100644
index 00000000000..e3c1f710aef
--- /dev/null
+++ b/tests/components/forecast_solar/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Forecast Solar integration."""
diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py
new file mode 100644
index 00000000000..c2b5fc08181
--- /dev/null
+++ b/tests/components/forecast_solar/conftest.py
@@ -0,0 +1,92 @@
+"""Fixtures for Forecast.Solar integration tests."""
+
+import datetime
+from typing import Generator
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from homeassistant.components.forecast_solar.const import (
+ CONF_AZIMUTH,
+ CONF_DAMPING,
+ CONF_DECLINATION,
+ CONF_MODULES_POWER,
+ DOMAIN,
+)
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
+from homeassistant.core import HomeAssistant
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture(autouse=True)
+async def mock_persistent_notification(hass: HomeAssistant) -> None:
+ """Set up component for persistent notifications."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+
+@pytest.fixture
+def mock_config_entry() -> MockConfigEntry:
+ """Return the default mocked config entry."""
+ return MockConfigEntry(
+ title="Green House",
+ unique_id="unique",
+ domain=DOMAIN,
+ data={
+ CONF_LATITUDE: 52.42,
+ CONF_LONGITUDE: 4.42,
+ },
+ options={
+ CONF_API_KEY: "abcdef12345",
+ CONF_DECLINATION: 30,
+ CONF_AZIMUTH: 190,
+ CONF_MODULES_POWER: 5100,
+ CONF_DAMPING: 0.5,
+ },
+ )
+
+
+@pytest.fixture
+def mock_forecast_solar() -> Generator[None, MagicMock, None]:
+ """Return a mocked Forecast.Solar client."""
+ with patch(
+ "homeassistant.components.forecast_solar.ForecastSolar", autospec=True
+ ) as forecast_solar_mock:
+ forecast_solar = forecast_solar_mock.return_value
+
+ estimate = MagicMock()
+ estimate.timezone = "Europe/Amsterdam"
+ estimate.energy_production_today = 100
+ estimate.energy_production_tomorrow = 200
+ estimate.power_production_now = 300
+ estimate.power_highest_peak_time_today = datetime.datetime(
+ 2021, 6, 27, 13, 0, tzinfo=datetime.timezone.utc
+ )
+ estimate.power_highest_peak_time_tomorrow = datetime.datetime(
+ 2021, 6, 27, 14, 0, tzinfo=datetime.timezone.utc
+ )
+ estimate.power_production_next_hour = 400
+ estimate.power_production_next_6hours = 500
+ estimate.power_production_next_12hours = 600
+ estimate.power_production_next_24hours = 700
+ estimate.energy_current_hour = 800
+ estimate.energy_next_hour = 900
+
+ forecast_solar.estimate.return_value = estimate
+ yield forecast_solar
+
+
+@pytest.fixture
+async def init_integration(
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+ mock_forecast_solar: MagicMock,
+) -> MockConfigEntry:
+ """Set up the Forecast.Solar integration for testing."""
+ mock_config_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ return mock_config_entry
diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py
new file mode 100644
index 00000000000..ac950d38b51
--- /dev/null
+++ b/tests/components/forecast_solar/test_config_flow.py
@@ -0,0 +1,89 @@
+"""Test the Forecast.Solar config flow."""
+from unittest.mock import patch
+
+from homeassistant.components.forecast_solar.const import (
+ CONF_AZIMUTH,
+ CONF_DAMPING,
+ CONF_DECLINATION,
+ CONF_MODULES_POWER,
+ DOMAIN,
+)
+from homeassistant.config_entries import SOURCE_USER
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+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_user_flow(hass: HomeAssistant) -> 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
+
+ with patch(
+ "homeassistant.components.forecast_solar.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_NAME: "Name",
+ CONF_LATITUDE: 52.42,
+ CONF_LONGITUDE: 4.42,
+ CONF_AZIMUTH: 142,
+ CONF_DECLINATION: 42,
+ CONF_MODULES_POWER: 4242,
+ },
+ )
+
+ assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
+ assert result2.get("title") == "Name"
+ assert result2.get("data") == {
+ CONF_LATITUDE: 52.42,
+ CONF_LONGITUDE: 4.42,
+ }
+ assert result2.get("options") == {
+ CONF_AZIMUTH: 142,
+ CONF_DECLINATION: 42,
+ CONF_MODULES_POWER: 4242,
+ }
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_options_flow(
+ hass: HomeAssistant, mock_config_entry: MockConfigEntry
+) -> None:
+ """Test config flow options."""
+ mock_config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
+
+ assert result.get("type") == RESULT_TYPE_FORM
+ assert result.get("step_id") == "init"
+ assert "flow_id" in result
+
+ result2 = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_API_KEY: "solarPOWER!",
+ CONF_DECLINATION: 21,
+ CONF_AZIMUTH: 22,
+ CONF_MODULES_POWER: 2122,
+ CONF_DAMPING: 0.25,
+ },
+ )
+
+ assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
+ assert result2.get("data") == {
+ CONF_API_KEY: "solarPOWER!",
+ CONF_DECLINATION: 21,
+ CONF_AZIMUTH: 22,
+ CONF_MODULES_POWER: 2122,
+ CONF_DAMPING: 0.25,
+ }
diff --git a/tests/components/forecast_solar/test_init.py b/tests/components/forecast_solar/test_init.py
new file mode 100644
index 00000000000..719041aaf58
--- /dev/null
+++ b/tests/components/forecast_solar/test_init.py
@@ -0,0 +1,46 @@
+"""Tests for the Forecast.Solar integration."""
+from unittest.mock import MagicMock, patch
+
+from forecast_solar import ForecastSolarConnectionError
+
+from homeassistant.components.forecast_solar.const import DOMAIN
+from homeassistant.config_entries import ConfigEntryState
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry
+
+
+async def test_load_unload_config_entry(
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+ mock_forecast_solar: MagicMock,
+) -> None:
+ """Test the Forecast.Solar 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 == 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)
+
+
+@patch(
+ "homeassistant.components.forecast_solar.ForecastSolar.estimate",
+ side_effect=ForecastSolarConnectionError,
+)
+async def test_config_entry_not_ready(
+ mock_request: MagicMock,
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+) -> None:
+ """Test the Forecast.Solar configuration entry not ready."""
+ mock_config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert mock_request.call_count == 1
+ assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py
new file mode 100644
index 00000000000..a3513b86a5d
--- /dev/null
+++ b/tests/components/forecast_solar/test_sensor.py
@@ -0,0 +1,228 @@
+"""Tests for the sensors provided by the Forecast.Solar integration."""
+from unittest.mock import MagicMock
+
+import pytest
+
+from homeassistant.components.forecast_solar.const import DOMAIN, ENTRY_TYPE_SERVICE
+from homeassistant.components.sensor import (
+ ATTR_STATE_CLASS,
+ DOMAIN as SENSOR_DOMAIN,
+ STATE_CLASS_MEASUREMENT,
+)
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_FRIENDLY_NAME,
+ ATTR_ICON,
+ ATTR_UNIT_OF_MEASUREMENT,
+ DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_POWER,
+ DEVICE_CLASS_TIMESTAMP,
+ ENERGY_KILO_WATT_HOUR,
+ POWER_WATT,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+
+from tests.common import MockConfigEntry
+
+
+async def test_sensors(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+) -> None:
+ """Test the Forecast.Solar sensors."""
+ entry_id = init_integration.entry_id
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
+
+ state = hass.states.get("sensor.energy_production_today")
+ entry = entity_registry.async_get("sensor.energy_production_today")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_energy_production_today"
+ assert state.state == "100"
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME)
+ == "Estimated Energy Production - Today"
+ )
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY
+ assert ATTR_ICON not in state.attributes
+
+ state = hass.states.get("sensor.energy_production_tomorrow")
+ entry = entity_registry.async_get("sensor.energy_production_tomorrow")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_energy_production_tomorrow"
+ assert state.state == "200"
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME)
+ == "Estimated Energy Production - Tomorrow"
+ )
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY
+ assert ATTR_ICON not in state.attributes
+
+ state = hass.states.get("sensor.power_highest_peak_time_today")
+ entry = entity_registry.async_get("sensor.power_highest_peak_time_today")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_power_highest_peak_time_today"
+ assert state.state == "2021-06-27 13:00:00+00:00"
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Today"
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP
+ assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
+ assert ATTR_ICON not in state.attributes
+
+ state = hass.states.get("sensor.power_highest_peak_time_tomorrow")
+ entry = entity_registry.async_get("sensor.power_highest_peak_time_tomorrow")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_power_highest_peak_time_tomorrow"
+ assert state.state == "2021-06-27 14:00:00+00:00"
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Tomorrow"
+ )
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP
+ assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
+ assert ATTR_ICON not in state.attributes
+
+ state = hass.states.get("sensor.power_production_now")
+ entry = entity_registry.async_get("sensor.power_production_now")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_power_production_now"
+ assert state.state == "300"
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Power Production - Now"
+ )
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
+ assert ATTR_ICON not in state.attributes
+
+ state = hass.states.get("sensor.energy_current_hour")
+ entry = entity_registry.async_get("sensor.energy_current_hour")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_energy_current_hour"
+ assert state.state == "800"
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME)
+ == "Estimated Energy Production - This Hour"
+ )
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY
+ assert ATTR_ICON not in state.attributes
+
+ state = hass.states.get("sensor.energy_next_hour")
+ entry = entity_registry.async_get("sensor.energy_next_hour")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_energy_next_hour"
+ assert state.state == "900"
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME)
+ == "Estimated Energy Production - Next Hour"
+ )
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY
+ assert ATTR_ICON not in state.attributes
+
+ assert entry.device_id
+ device_entry = device_registry.async_get(entry.device_id)
+ assert device_entry
+ assert device_entry.identifiers == {(DOMAIN, f"{entry_id}")}
+ assert device_entry.manufacturer == "Forecast.Solar"
+ assert device_entry.name == "Solar Production Forecast"
+ assert device_entry.entry_type == ENTRY_TYPE_SERVICE
+ assert not device_entry.model
+ assert not device_entry.sw_version
+
+
+@pytest.mark.parametrize(
+ "entity_id",
+ (
+ "sensor.power_production_next_12hours",
+ "sensor.power_production_next_24hours",
+ "sensor.power_production_next_hour",
+ ),
+)
+async def test_disabled_by_default(
+ hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str
+) -> None:
+ """Test the Forecast.Solar sensors that are disabled by default."""
+ entity_registry = er.async_get(hass)
+
+ state = hass.states.get(entity_id)
+ assert state is None
+
+ entry = entity_registry.async_get(entity_id)
+ assert entry
+ assert entry.disabled
+ assert entry.disabled_by == er.DISABLED_INTEGRATION
+
+
+@pytest.mark.parametrize(
+ "key,name,value",
+ [
+ (
+ "power_production_next_12hours",
+ "Estimated Power Production - Next 12 Hours",
+ "600",
+ ),
+ (
+ "power_production_next_24hours",
+ "Estimated Power Production - Next 24 Hours",
+ "700",
+ ),
+ (
+ "power_production_next_hour",
+ "Estimated Power Production - Next Hour",
+ "400",
+ ),
+ ],
+)
+async def test_enabling_disable_by_default(
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+ mock_forecast_solar: MagicMock,
+ key: str,
+ name: str,
+ value: str,
+) -> None:
+ """Test the Forecast.Solar sensors that are disabled by default."""
+ entry_id = mock_config_entry.entry_id
+ entity_id = f"{SENSOR_DOMAIN}.{key}"
+ entity_registry = er.async_get(hass)
+
+ # Pre-create registry entry for disabled by default sensor
+ entity_registry.async_get_or_create(
+ SENSOR_DOMAIN,
+ DOMAIN,
+ f"{entry_id}_{key}",
+ suggested_object_id=key,
+ disabled_by=None,
+ )
+
+ mock_config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity_id)
+ entry = entity_registry.async_get(entity_id)
+ assert entry
+ assert state
+ assert entry.unique_id == f"{entry_id}_{key}"
+ assert state.state == value
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == name
+ assert state.attributes.get(ATTR_STATE_CLASS) is None
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
+ assert ATTR_ICON not in state.attributes
diff --git a/tests/components/freedompro/__init__.py b/tests/components/freedompro/__init__.py
new file mode 100644
index 00000000000..1f87c43b43c
--- /dev/null
+++ b/tests/components/freedompro/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Freedompro integration."""
diff --git a/tests/components/freedompro/conftest.py b/tests/components/freedompro/conftest.py
new file mode 100644
index 00000000000..c43887fa487
--- /dev/null
+++ b/tests/components/freedompro/conftest.py
@@ -0,0 +1,67 @@
+"""Fixtures for Freedompro integration tests."""
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant.components.freedompro.const import DOMAIN
+
+from tests.common import MockConfigEntry
+from tests.components.freedompro.const import DEVICES, DEVICES_STATE
+
+
+@pytest.fixture
+async def init_integration(hass) -> MockConfigEntry:
+ """Set up the Freedompro integration in Home Assistant."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="Feedompro",
+ unique_id="0123456",
+ data={
+ "api_key": "gdhsksjdhcncjdkdjndjdkdmndjdjdkd",
+ },
+ )
+
+ with patch(
+ "homeassistant.components.freedompro.get_list",
+ return_value={
+ "state": True,
+ "devices": DEVICES,
+ },
+ ), patch(
+ "homeassistant.components.freedompro.get_states",
+ return_value=DEVICES_STATE,
+ ):
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ return entry
+
+
+@pytest.fixture
+async def init_integration_no_state(hass) -> MockConfigEntry:
+ """Set up the Freedompro integration in Home Assistant without state."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="Feedompro",
+ unique_id="0123456",
+ data={
+ "api_key": "gdhsksjdhcncjdkdjndjdkdmndjdjdkd",
+ },
+ )
+
+ with patch(
+ "homeassistant.components.freedompro.get_list",
+ return_value={
+ "state": True,
+ "devices": DEVICES,
+ },
+ ), patch(
+ "homeassistant.components.freedompro.get_states",
+ return_value=[],
+ ):
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ return entry
diff --git a/tests/components/freedompro/const.py b/tests/components/freedompro/const.py
new file mode 100644
index 00000000000..8635858d000
--- /dev/null
+++ b/tests/components/freedompro/const.py
@@ -0,0 +1,219 @@
+"""Const Freedompro for test."""
+
+DEVICES = [
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0",
+ "name": "Bathroom leak sensor",
+ "type": "leakSensor",
+ "characteristics": ["leakDetected"],
+ },
+ {
+ "uid": "2WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0",
+ "name": "lock",
+ "type": "lock",
+ "characteristics": ["lock"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*ILYH1E3DWZOVMNEUIMDYMNLOW-LFRQFDPWWJOVHVDOS",
+ "name": "Bedroom fan",
+ "type": "fan",
+ "characteristics": ["on", "rotationSpeed"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SOT3NKALCRQMHUHJUF79NUG6UQP1IIQIN1PJVRRPT0C",
+ "name": "Contact sensor living room",
+ "type": "contactSensor",
+ "characteristics": ["contactSensorState"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*VTEPEDYE8DXGS8U94CJKQDLKMN6CUX1IJWSOER2HZCK",
+ "name": "Doorway motion sensor",
+ "type": "motionSensor",
+ "characteristics": ["motionDetected"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*QN-DDFMPEPRDOQV7W7JQG3NL0NPZGTLIBYT3HFSPNEY",
+ "name": "Garden humidity sensor",
+ "type": "humiditySensor",
+ "characteristics": ["currentRelativeHumidity"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*1JKU1MVWHQL-Z9SCUS85VFXMRGNDCDNDDUVVDKBU31W",
+ "name": "Irrigation switch",
+ "type": "switch",
+ "characteristics": ["on"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M",
+ "name": "lightbulb",
+ "type": "lightbulb",
+ "characteristics": ["on", "brightness", "saturation", "hue"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SNG7Y3R1R0S_W5BCNPP1O5WUN2NCEOOT27EFSYT6JYS",
+ "name": "Living room occupancy sensor",
+ "type": "occupancySensor",
+ "characteristics": ["occupancyDetected"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*LWPVY7X1AX0DRWLYUUNZ3ZSTHMYNDDBQTPZCZQUUASA",
+ "name": "Living room temperature sensor",
+ "type": "temperatureSensor",
+ "characteristics": ["currentTemperature"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SXFMEXI4UMDBAMXXPI6LJV47O9NY-IRCAKZI7_MW0LY",
+ "name": "Smoke sensor kitchen",
+ "type": "smokeSensor",
+ "characteristics": ["smokeDetected"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*R6V0FNNF7SACWZ8V9NCOX7UCYI4ODSYAOJWZ80PLJ3C",
+ "name": "Bedroom CO2 sensor",
+ "type": "carbonDioxideSensor",
+ "characteristics": ["carbonDioxideDetected", "carbonDioxideLevel"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3-QURR5Q6ADA8ML1TBRG59RRGM1F9LVUZLKPYKFJQHC",
+ "name": "bedroomlight",
+ "type": "lightbulb",
+ "characteristics": ["on"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*TWMYQKL3UVED4HSIIB9GXJWJZBQCXG-9VE-N2IUAIWI",
+ "name": "Bedroom thermostat",
+ "type": "thermostat",
+ "characteristics": [
+ "heatingCoolingState",
+ "currentTemperature",
+ "targetTemperature",
+ ],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS",
+ "name": "Bedroom window covering",
+ "type": "windowCovering",
+ "characteristics": ["position"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JVRAR_6WVL1Y0PJ5GFWGPMFV7FLVD4MZKBWXC_UFWYM",
+ "name": "Garden light sensors",
+ "type": "lightSensor",
+ "characteristics": ["currentAmbientLightLevel"],
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*0PUTVZVJJJL-ZHZZBHTIBS3-J-U7JYNPACFPJW0MD-I",
+ "name": "Living room outlet",
+ "type": "outlet",
+ "characteristics": ["on"],
+ },
+]
+
+DEVICES_STATE = [
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0",
+ "type": "leakSensor",
+ "state": {"leakDetected": 0},
+ "online": True,
+ },
+ {
+ "uid": "2WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0",
+ "type": "lock",
+ "state": {"lock": 0},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*ILYH1E3DWZOVMNEUIMDYMNLOW-LFRQFDPWWJOVHVDOS",
+ "type": "fan",
+ "state": {"on": False, "rotationSpeed": 0},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SOT3NKALCRQMHUHJUF79NUG6UQP1IIQIN1PJVRRPT0C",
+ "type": "contactSensor",
+ "state": {"contactSensorState": True},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*VTEPEDYE8DXGS8U94CJKQDLKMN6CUX1IJWSOER2HZCK",
+ "type": "motionSensor",
+ "state": {"motionDetected": False},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*QN-DDFMPEPRDOQV7W7JQG3NL0NPZGTLIBYT3HFSPNEY",
+ "type": "humiditySensor",
+ "state": {"currentRelativeHumidity": 0},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*1JKU1MVWHQL-Z9SCUS85VFXMRGNDCDNDDUVVDKBU31W",
+ "type": "switch",
+ "state": {"on": False},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M",
+ "type": "lightbulb",
+ "state": {"on": True, "brightness": 0, "saturation": 0, "hue": 0},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SNG7Y3R1R0S_W5BCNPP1O5WUN2NCEOOT27EFSYT6JYS",
+ "type": "occupancySensor",
+ "state": {"occupancyDetected": False},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*LWPVY7X1AX0DRWLYUUNZ3ZSTHMYNDDBQTPZCZQUUASA",
+ "type": "temperatureSensor",
+ "state": {"currentTemperature": 0},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SXFMEXI4UMDBAMXXPI6LJV47O9NY-IRCAKZI7_MW0LY",
+ "type": "smokeSensor",
+ "state": {"smokeDetected": False},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*R6V0FNNF7SACWZ8V9NCOX7UCYI4ODSYAOJWZ80PLJ3C",
+ "type": "carbonDioxideSensor",
+ "state": {"carbonDioxideDetected": False, "carbonDioxideLevel": 0},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3-QURR5Q6ADA8ML1TBRG59RRGM1F9LVUZLKPYKFJQHC",
+ "type": "lightbulb",
+ "state": {"on": False},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*TWMYQKL3UVED4HSIIB9GXJWJZBQCXG-9VE-N2IUAIWI",
+ "type": "thermostat",
+ "state": {
+ "heatingCoolingState": 1,
+ "currentTemperature": 14,
+ "targetTemperature": 14,
+ },
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS",
+ "type": "windowCovering",
+ "state": {"position": 0},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JVRAR_6WVL1Y0PJ5GFWGPMFV7FLVD4MZKBWXC_UFWYM",
+ "type": "lightSensor",
+ "state": {"currentAmbientLightLevel": 500},
+ "online": True,
+ },
+ {
+ "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*0PUTVZVJJJL-ZHZZBHTIBS3-J-U7JYNPACFPJW0MD-I",
+ "type": "outlet",
+ "state": {"on": False},
+ "online": True,
+ },
+]
diff --git a/tests/components/freedompro/test_config_flow.py b/tests/components/freedompro/test_config_flow.py
new file mode 100644
index 00000000000..f44cbd232ad
--- /dev/null
+++ b/tests/components/freedompro/test_config_flow.py
@@ -0,0 +1,82 @@
+"""Define tests for the Freedompro config flow."""
+from unittest.mock import patch
+
+from homeassistant import data_entry_flow
+from homeassistant.components.freedompro.const import DOMAIN
+from homeassistant.config_entries import SOURCE_USER
+from homeassistant.const import CONF_API_KEY
+
+from tests.components.freedompro.const import DEVICES
+
+VALID_CONFIG = {
+ CONF_API_KEY: "ksdjfgslkjdfksjdfksjgfksjd",
+}
+
+
+async def test_show_form(hass):
+ """Test that the form is served with no input."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == SOURCE_USER
+
+
+async def test_invalid_auth(hass):
+ """Test that errors are shown when API key is invalid."""
+ with patch(
+ "homeassistant.components.freedompro.config_flow.list",
+ return_value={
+ "state": False,
+ "code": -201,
+ },
+ ):
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=VALID_CONFIG,
+ )
+
+ assert result["errors"] == {"base": "invalid_auth"}
+
+
+async def test_connection_error(hass):
+ """Test that errors are shown when API key is invalid."""
+ with patch(
+ "homeassistant.components.freedompro.config_flow.get_list",
+ return_value={
+ "state": False,
+ "code": -200,
+ },
+ ):
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=VALID_CONFIG,
+ )
+
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_create_entry(hass):
+ """Test that the user step works."""
+ with patch(
+ "homeassistant.components.freedompro.config_flow.get_list",
+ return_value={
+ "state": True,
+ "devices": DEVICES,
+ },
+ ):
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=VALID_CONFIG,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "Freedompro"
+ assert result["data"][CONF_API_KEY] == "ksdjfgslkjdfksjdfksjgfksjd"
diff --git a/tests/components/freedompro/test_init.py b/tests/components/freedompro/test_init.py
new file mode 100644
index 00000000000..4e9d9ec197d
--- /dev/null
+++ b/tests/components/freedompro/test_init.py
@@ -0,0 +1,55 @@
+"""Freedompro component tests."""
+import logging
+from unittest.mock import patch
+
+from homeassistant.components.freedompro.const import DOMAIN
+from homeassistant.config_entries import ConfigEntryState
+
+from tests.common import MockConfigEntry
+
+LOGGER = logging.getLogger(__name__)
+
+ENTITY_ID = f"{DOMAIN}.fake_name"
+
+
+async def test_async_setup_entry(hass, init_integration):
+ """Test a successful setup entry."""
+ entry = init_integration
+ assert entry is not None
+ state = hass.states
+ assert state is not None
+
+
+async def test_config_not_ready(hass):
+ """Test for setup failure if connection to Freedompro is missing."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="Feedompro",
+ unique_id="0123456",
+ data={
+ "api_key": "gdhsksjdhcncjdkdjndjdkdmndjdjdkd",
+ },
+ )
+
+ with patch(
+ "homeassistant.components.freedompro.get_list",
+ return_value={
+ "state": False,
+ },
+ ):
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ assert entry.state == ConfigEntryState.SETUP_RETRY
+
+
+async def test_unload_entry(hass, init_integration):
+ """Test successful unload of entry."""
+ entry = init_integration
+
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ assert entry.state == ConfigEntryState.LOADED
+
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert entry.state == ConfigEntryState.NOT_LOADED
diff --git a/tests/components/freedompro/test_light.py b/tests/components/freedompro/test_light.py
new file mode 100644
index 00000000000..09a945ada03
--- /dev/null
+++ b/tests/components/freedompro/test_light.py
@@ -0,0 +1,155 @@
+"""Tests for the Freedompro light."""
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_HS_COLOR,
+ DOMAIN as LIGHT_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
+
+
+async def test_light_get_state(hass, init_integration):
+ """Test states of the light."""
+ init_integration
+ registry = er.async_get(hass)
+
+ entity_id = "light.lightbulb"
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_ON
+ assert state.attributes.get("friendly_name") == "lightbulb"
+
+ entry = registry.async_get(entity_id)
+ assert entry
+ assert (
+ entry.unique_id
+ == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M"
+ )
+
+
+async def test_light_set_on(hass, init_integration):
+ """Test set on of the light."""
+ init_integration
+ registry = er.async_get(hass)
+
+ entity_id = "light.lightbulb"
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_ON
+ assert state.attributes.get("friendly_name") == "lightbulb"
+
+ entry = registry.async_get(entity_id)
+ assert entry
+ assert (
+ entry.unique_id
+ == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M"
+ )
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: [entity_id]},
+ blocking=True,
+ )
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_ON
+
+
+async def test_light_set_off(hass, init_integration):
+ """Test set off of the light."""
+ init_integration
+ registry = er.async_get(hass)
+
+ entity_id = "light.bedroomlight"
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_OFF
+ assert state.attributes.get("friendly_name") == "bedroomlight"
+
+ entry = registry.async_get(entity_id)
+ assert entry
+ assert (
+ entry.unique_id
+ == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3-QURR5Q6ADA8ML1TBRG59RRGM1F9LVUZLKPYKFJQHC"
+ )
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: [entity_id]},
+ blocking=True,
+ )
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_OFF
+
+
+async def test_light_set_brightness(hass, init_integration):
+ """Test set brightness of the light."""
+ init_integration
+ registry = er.async_get(hass)
+
+ entity_id = "light.lightbulb"
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_ON
+ assert state.attributes.get("friendly_name") == "lightbulb"
+
+ entry = registry.async_get(entity_id)
+ assert entry
+ assert (
+ entry.unique_id
+ == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M"
+ )
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: [entity_id], ATTR_BRIGHTNESS: 255},
+ blocking=True,
+ )
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_ON
+ assert int(state.attributes[ATTR_BRIGHTNESS]) == 0
+
+
+async def test_light_set_hue(hass, init_integration):
+ """Test set brightness of the light."""
+ init_integration
+ registry = er.async_get(hass)
+
+ entity_id = "light.lightbulb"
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_ON
+ assert state.attributes.get("friendly_name") == "lightbulb"
+
+ entry = registry.async_get(entity_id)
+ assert entry
+ assert (
+ entry.unique_id
+ == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M"
+ )
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: [entity_id],
+ ATTR_BRIGHTNESS: 255,
+ ATTR_HS_COLOR: (352.32, 100.0),
+ },
+ blocking=True,
+ )
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_ON
+ assert int(state.attributes[ATTR_BRIGHTNESS]) == 0
+ assert state.attributes[ATTR_HS_COLOR] == (0, 0)
diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py
index 6e051ef1bdd..1551a508277 100644
--- a/tests/components/fritz/test_config_flow.py
+++ b/tests/components/fritz/test_config_flow.py
@@ -56,6 +56,8 @@ MOCK_SSDP_DATA = {
ATTR_UPNP_UDN: "uuid:only-a-test",
}
+MOCK_REQUEST = b'xxxxxxxxxxxxxxxxxxxxxxxx0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2FakeFritzUser\n'
+
@pytest.fixture()
def fc_class_mock():
@@ -72,7 +74,16 @@ async def test_user(hass: HomeAssistant, fc_class_mock):
side_effect=fc_class_mock,
), patch("homeassistant.components.fritz.common.FritzStatus"), patch(
"homeassistant.components.fritz.async_setup_entry"
- ) as mock_setup_entry:
+ ) as mock_setup_entry, patch(
+ "requests.get"
+ ) as mock_request_get, patch(
+ "requests.post"
+ ) as mock_request_post:
+
+ mock_request_get.return_value.status_code = 200
+ mock_request_get.return_value.content = MOCK_REQUEST
+ mock_request_post.return_value.status_code = 200
+ mock_request_post.return_value.text = MOCK_REQUEST
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@@ -106,7 +117,16 @@ async def test_user_already_configured(hass: HomeAssistant, fc_class_mock):
with patch(
"homeassistant.components.fritz.common.FritzConnection",
side_effect=fc_class_mock,
- ), patch("homeassistant.components.fritz.common.FritzStatus"):
+ ), patch("homeassistant.components.fritz.common.FritzStatus"), patch(
+ "requests.get"
+ ) as mock_request_get, patch(
+ "requests.post"
+ ) as mock_request_post:
+
+ mock_request_get.return_value.status_code = 200
+ mock_request_get.return_value.content = MOCK_REQUEST
+ mock_request_post.return_value.status_code = 200
+ mock_request_post.return_value.text = MOCK_REQUEST
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@@ -202,7 +222,16 @@ async def test_reauth_successful(hass: HomeAssistant, fc_class_mock):
side_effect=fc_class_mock,
), patch("homeassistant.components.fritz.common.FritzStatus"), patch(
"homeassistant.components.fritz.async_setup_entry"
- ) as mock_setup_entry:
+ ) as mock_setup_entry, patch(
+ "requests.get"
+ ) as mock_request_get, patch(
+ "requests.post"
+ ) as mock_request_post:
+
+ mock_request_get.return_value.status_code = 200
+ mock_request_get.return_value.content = MOCK_REQUEST
+ mock_request_post.return_value.status_code = 200
+ mock_request_post.return_value.text = MOCK_REQUEST
result = await hass.config_entries.flow.async_init(
DOMAIN,
@@ -305,7 +334,7 @@ async def test_ssdp_already_configured_host(hass: HomeAssistant, fc_class_mock):
async def test_ssdp_already_configured_host_uuid(hass: HomeAssistant, fc_class_mock):
- """Test starting a flow from discovery with a laready configured uuid."""
+ """Test starting a flow from discovery with an already configured uuid."""
mock_config = MockConfigEntry(
domain=DOMAIN,
@@ -355,7 +384,16 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock):
side_effect=fc_class_mock,
), patch("homeassistant.components.fritz.common.FritzStatus"), patch(
"homeassistant.components.fritz.async_setup_entry"
- ) as mock_setup_entry:
+ ) as mock_setup_entry, patch(
+ "requests.get"
+ ) as mock_request_get, patch(
+ "requests.post"
+ ) as mock_request_post:
+
+ mock_request_get.return_value.status_code = 200
+ mock_request_get.return_value.content = MOCK_REQUEST
+ mock_request_post.return_value.status_code = 200
+ mock_request_post.return_value.text = MOCK_REQUEST
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA
@@ -411,7 +449,16 @@ async def test_import(hass: HomeAssistant, fc_class_mock):
side_effect=fc_class_mock,
), patch("homeassistant.components.fritz.common.FritzStatus"), patch(
"homeassistant.components.fritz.async_setup_entry"
- ) as mock_setup_entry:
+ ) as mock_setup_entry, patch(
+ "requests.get"
+ ) as mock_request_get, patch(
+ "requests.post"
+ ) as mock_request_post:
+
+ mock_request_get.return_value.status_code = 200
+ mock_request_get.return_value.content = MOCK_REQUEST
+ mock_request_post.return_value.status_code = 200
+ mock_request_post.return_value.text = MOCK_REQUEST
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_CONFIG
diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py
index 123ca120243..a8b44511fb2 100644
--- a/tests/components/google_assistant/__init__.py
+++ b/tests/components/google_assistant/__init__.py
@@ -247,14 +247,20 @@ DEMO_DEVICES = [
{
"id": "fan.living_room_fan",
"name": {"name": "Living Room Fan"},
- "traits": ["action.devices.traits.FanSpeed", "action.devices.traits.OnOff"],
+ "traits": [
+ "action.devices.traits.FanSpeed",
+ "action.devices.traits.OnOff",
+ ],
"type": "action.devices.types.FAN",
"willReportState": False,
},
{
"id": "fan.ceiling_fan",
"name": {"name": "Ceiling Fan"},
- "traits": ["action.devices.traits.FanSpeed", "action.devices.traits.OnOff"],
+ "traits": [
+ "action.devices.traits.FanSpeed",
+ "action.devices.traits.OnOff",
+ ],
"type": "action.devices.types.FAN",
"willReportState": False,
},
@@ -275,7 +281,10 @@ DEMO_DEVICES = [
{
"id": "fan.preset_only_limited_fan",
"name": {"name": "Preset Only Limited Fan"},
- "traits": ["action.devices.traits.OnOff"],
+ "traits": [
+ "action.devices.traits.OnOff",
+ "action.devices.traits.Modes",
+ ],
"type": "action.devices.types.FAN",
"willReportState": False,
},
@@ -402,4 +411,15 @@ DEMO_DEVICES = [
"type": "action.devices.types.LIGHT",
"willReportState": False,
},
+ {
+ "id": "light.entrance_color_white_lights",
+ "name": {"name": "Entrance Color + White Lights"},
+ "traits": [
+ "action.devices.traits.OnOff",
+ "action.devices.traits.Brightness",
+ "action.devices.traits.ColorSetting",
+ ],
+ "type": "action.devices.types.LIGHT",
+ "willReportState": False,
+ },
]
diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py
index c3678e7f99a..e2821d207d5 100644
--- a/tests/components/google_assistant/test_trait.py
+++ b/tests/components/google_assistant/test_trait.py
@@ -18,6 +18,7 @@ from homeassistant.components import (
media_player,
scene,
script,
+ select,
sensor,
switch,
vacuum,
@@ -1429,6 +1430,7 @@ async def test_fan_speed(hass):
],
"speed": "low",
"percentage": 33,
+ "percentage_step": 1.0,
},
),
BASIC_CONFIG,
@@ -1798,6 +1800,80 @@ async def test_modes_input_select(hass):
assert calls[0].data == {"entity_id": "input_select.bla", "option": "xyz"}
+async def test_modes_select(hass):
+ """Test Select Mode trait."""
+ assert helpers.get_google_type(select.DOMAIN, None) is not None
+ assert trait.ModesTrait.supported(select.DOMAIN, None, None, None)
+
+ trt = trait.ModesTrait(
+ hass,
+ State("select.bla", "unavailable"),
+ BASIC_CONFIG,
+ )
+ assert trt.sync_attributes() == {"availableModes": []}
+
+ trt = trait.ModesTrait(
+ hass,
+ State(
+ "select.bla",
+ "abc",
+ attributes={select.ATTR_OPTIONS: ["abc", "123", "xyz"]},
+ ),
+ BASIC_CONFIG,
+ )
+
+ attribs = trt.sync_attributes()
+ assert attribs == {
+ "availableModes": [
+ {
+ "name": "option",
+ "name_values": [
+ {
+ "name_synonym": ["option", "setting", "mode", "value"],
+ "lang": "en",
+ }
+ ],
+ "settings": [
+ {
+ "setting_name": "abc",
+ "setting_values": [{"setting_synonym": ["abc"], "lang": "en"}],
+ },
+ {
+ "setting_name": "123",
+ "setting_values": [{"setting_synonym": ["123"], "lang": "en"}],
+ },
+ {
+ "setting_name": "xyz",
+ "setting_values": [{"setting_synonym": ["xyz"], "lang": "en"}],
+ },
+ ],
+ "ordered": False,
+ }
+ ]
+ }
+
+ assert trt.query_attributes() == {
+ "currentModeSettings": {"option": "abc"},
+ "on": True,
+ }
+
+ assert trt.can_execute(
+ trait.COMMAND_MODES,
+ params={"updateModeSettings": {"option": "xyz"}},
+ )
+
+ calls = async_mock_service(hass, select.DOMAIN, select.SERVICE_SELECT_OPTION)
+ await trt.execute(
+ trait.COMMAND_MODES,
+ BASIC_DATA,
+ {"updateModeSettings": {"option": "xyz"}},
+ {},
+ )
+
+ assert len(calls) == 1
+ assert calls[0].data == {"entity_id": "select.bla", "option": "xyz"}
+
+
async def test_modes_humidifier(hass):
"""Test Humidifier Mode trait."""
assert helpers.get_google_type(humidifier.DOMAIN, None) is not None
@@ -1951,6 +2027,97 @@ async def test_sound_modes(hass):
}
+async def test_preset_modes(hass):
+ """Test Mode trait for fan preset modes."""
+ assert helpers.get_google_type(fan.DOMAIN, None) is not None
+ assert trait.ModesTrait.supported(fan.DOMAIN, fan.SUPPORT_PRESET_MODE, None, None)
+
+ trt = trait.ModesTrait(
+ hass,
+ State(
+ "fan.living_room",
+ STATE_ON,
+ attributes={
+ fan.ATTR_PRESET_MODES: ["auto", "whoosh"],
+ fan.ATTR_PRESET_MODE: "auto",
+ ATTR_SUPPORTED_FEATURES: fan.SUPPORT_PRESET_MODE,
+ },
+ ),
+ BASIC_CONFIG,
+ )
+
+ attribs = trt.sync_attributes()
+ assert attribs == {
+ "availableModes": [
+ {
+ "name": "preset mode",
+ "name_values": [
+ {"name_synonym": ["preset mode", "mode", "preset"], "lang": "en"}
+ ],
+ "settings": [
+ {
+ "setting_name": "auto",
+ "setting_values": [{"setting_synonym": ["auto"], "lang": "en"}],
+ },
+ {
+ "setting_name": "whoosh",
+ "setting_values": [
+ {"setting_synonym": ["whoosh"], "lang": "en"}
+ ],
+ },
+ ],
+ "ordered": False,
+ }
+ ]
+ }
+
+ assert trt.query_attributes() == {
+ "currentModeSettings": {"preset mode": "auto"},
+ "on": True,
+ }
+
+ assert trt.can_execute(
+ trait.COMMAND_MODES,
+ params={"updateModeSettings": {"preset mode": "auto"}},
+ )
+
+ calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PRESET_MODE)
+ await trt.execute(
+ trait.COMMAND_MODES,
+ BASIC_DATA,
+ {"updateModeSettings": {"preset mode": "auto"}},
+ {},
+ )
+
+ assert len(calls) == 1
+ assert calls[0].data == {
+ "entity_id": "fan.living_room",
+ "preset_mode": "auto",
+ }
+
+
+async def test_traits_unknown_domains(hass, caplog):
+ """Test Mode trait for unsupported domain."""
+ trt = trait.ModesTrait(
+ hass,
+ State(
+ "switch.living_room",
+ STATE_ON,
+ ),
+ BASIC_CONFIG,
+ )
+
+ assert trt.supported("not_supported_domain", False, None, None) is False
+ await trt.execute(
+ trait.COMMAND_MODES,
+ BASIC_DATA,
+ {"updateModeSettings": {}},
+ {},
+ )
+ assert "Received an Options command for unrecognised domain" in caplog.text
+ caplog.clear()
+
+
async def test_openclose_cover(hass):
"""Test OpenClose trait support for cover domain."""
assert helpers.get_google_type(cover.DOMAIN, None) is not None
diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py
index 62dd7ca545f..c062cfc5615 100644
--- a/tests/components/gree/test_climate.py
+++ b/tests/components/gree/test_climate.py
@@ -55,6 +55,8 @@ from homeassistant.const import (
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_UNAVAILABLE,
+ TEMP_CELSIUS,
+ TEMP_FAHRENHEIT,
)
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -376,26 +378,42 @@ async def test_send_power_off_device_timeout(hass, discovery, device, mock_now):
assert state.state == HVAC_MODE_OFF
-async def test_send_target_temperature(hass, discovery, device, mock_now):
+@pytest.mark.parametrize(
+ "units,temperature", [(TEMP_CELSIUS, 25), (TEMP_FAHRENHEIT, 74)]
+)
+async def test_send_target_temperature(hass, discovery, device, units, temperature):
"""Test for sending target temperature command to the device."""
+ hass.config.units.temperature_unit = units
+ if units == TEMP_FAHRENHEIT:
+ device().temperature_units = 1
+
await async_setup_gree(hass)
assert await hass.services.async_call(
DOMAIN,
SERVICE_SET_TEMPERATURE,
- {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 25.1},
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: temperature},
blocking=True,
)
state = hass.states.get(ENTITY_ID)
assert state is not None
- assert state.attributes.get(ATTR_TEMPERATURE) == 25
+ assert state.attributes.get(ATTR_TEMPERATURE) == temperature
+
+ # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component.
+ hass.config.units.temperature_unit = TEMP_CELSIUS
+@pytest.mark.parametrize(
+ "units,temperature", [(TEMP_CELSIUS, 25), (TEMP_FAHRENHEIT, 74)]
+)
async def test_send_target_temperature_device_timeout(
- hass, discovery, device, mock_now
+ hass, discovery, device, units, temperature
):
"""Test for sending target temperature command to the device with a device timeout."""
+ hass.config.units.temperature_unit = units
+ if units == TEMP_FAHRENHEIT:
+ device().temperature_units = 1
device().push_state_update.side_effect = DeviceTimeoutError
await async_setup_gree(hass)
@@ -403,24 +421,36 @@ async def test_send_target_temperature_device_timeout(
assert await hass.services.async_call(
DOMAIN,
SERVICE_SET_TEMPERATURE,
- {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 25.1},
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: temperature},
blocking=True,
)
state = hass.states.get(ENTITY_ID)
assert state is not None
- assert state.attributes.get(ATTR_TEMPERATURE) == 25
+ assert state.attributes.get(ATTR_TEMPERATURE) == temperature
+
+ # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component.
+ hass.config.units.temperature_unit = TEMP_CELSIUS
-async def test_update_target_temperature(hass, discovery, device, mock_now):
+@pytest.mark.parametrize(
+ "units,temperature", [(TEMP_CELSIUS, 25), (TEMP_FAHRENHEIT, 74)]
+)
+async def test_update_target_temperature(hass, discovery, device, units, temperature):
"""Test for updating target temperature from the device."""
- device().target_temperature = 32
+ hass.config.units.temperature_unit = units
+ if units == TEMP_FAHRENHEIT:
+ device().temperature_units = 1
+ device().target_temperature = temperature
await async_setup_gree(hass)
state = hass.states.get(ENTITY_ID)
assert state is not None
- assert state.attributes.get(ATTR_TEMPERATURE) == 32
+ assert state.attributes.get(ATTR_TEMPERATURE) == temperature
+
+ # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component.
+ hass.config.units.temperature_unit = TEMP_CELSIUS
@pytest.mark.parametrize(
diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py
new file mode 100644
index 00000000000..5dd5e4225cc
--- /dev/null
+++ b/tests/components/group/test_media_player.py
@@ -0,0 +1,516 @@
+"""The tests for the Media group platform."""
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant.components.group import DOMAIN
+from homeassistant.components.media_player import (
+ ATTR_MEDIA_CONTENT_TYPE,
+ ATTR_MEDIA_SEEK_POSITION,
+ ATTR_MEDIA_SHUFFLE,
+ ATTR_MEDIA_VOLUME_LEVEL,
+ DOMAIN as MEDIA_DOMAIN,
+ SERVICE_MEDIA_PAUSE,
+ SERVICE_MEDIA_SEEK,
+ SERVICE_PLAY_MEDIA,
+ SERVICE_SHUFFLE_SET,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ SERVICE_VOLUME_SET,
+ SUPPORT_PAUSE,
+ SUPPORT_PLAY,
+ SUPPORT_PLAY_MEDIA,
+ SUPPORT_SEEK,
+ SUPPORT_STOP,
+ SUPPORT_VOLUME_MUTE,
+ SUPPORT_VOLUME_SET,
+ SUPPORT_VOLUME_STEP,
+)
+from homeassistant.components.media_player.const import (
+ ATTR_MEDIA_CONTENT_ID,
+ ATTR_MEDIA_TRACK,
+ ATTR_MEDIA_VOLUME_MUTED,
+ SERVICE_CLEAR_PLAYLIST,
+)
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
+ SERVICE_MEDIA_NEXT_TRACK,
+ SERVICE_MEDIA_PLAY,
+ SERVICE_MEDIA_PREVIOUS_TRACK,
+ SERVICE_MEDIA_STOP,
+ SERVICE_VOLUME_DOWN,
+ SERVICE_VOLUME_MUTE,
+ SERVICE_VOLUME_UP,
+ STATE_OFF,
+ STATE_ON,
+ STATE_PAUSED,
+ STATE_PLAYING,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+)
+from homeassistant.setup import async_setup_component
+
+
+@pytest.fixture(name="mock_media_seek")
+def media_player_media_seek_fixture():
+ """Mock demo YouTube player media seek."""
+ with patch(
+ "homeassistant.components.demo.media_player.DemoYoutubePlayer.media_seek",
+ autospec=True,
+ ) as seek:
+ yield seek
+
+
+async def test_default_state(hass):
+ """Test media group default state."""
+ hass.states.async_set("media_player.player_1", "on")
+ await async_setup_component(
+ hass,
+ MEDIA_DOMAIN,
+ {
+ MEDIA_DOMAIN: {
+ "platform": DOMAIN,
+ "entities": ["media_player.player_1", "media_player.player_2"],
+ "name": "Media group",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("media_player.media_group")
+ assert state is not None
+ assert state.state == STATE_ON
+ assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
+ assert state.attributes.get(ATTR_ENTITY_ID) == [
+ "media_player.player_1",
+ "media_player.player_2",
+ ]
+
+
+async def test_state_reporting(hass):
+ """Test the state reporting."""
+ await async_setup_component(
+ hass,
+ MEDIA_DOMAIN,
+ {
+ MEDIA_DOMAIN: {
+ "platform": DOMAIN,
+ "entities": ["media_player.player_1", "media_player.player_2"],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert hass.states.get("media_player.media_group").state == STATE_UNKNOWN
+
+ hass.states.async_set("media_player.player_1", STATE_ON)
+ hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE)
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.media_group").state == STATE_ON
+
+ hass.states.async_set("media_player.player_1", STATE_ON)
+ hass.states.async_set("media_player.player_2", STATE_OFF)
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.media_group").state == STATE_ON
+
+ hass.states.async_set("media_player.player_1", STATE_OFF)
+ hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE)
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.media_group").state == STATE_OFF
+
+ hass.states.async_set("media_player.player_1", STATE_UNAVAILABLE)
+ hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE)
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.media_group").state == STATE_UNAVAILABLE
+
+
+async def test_supported_features(hass):
+ """Test supported features reporting."""
+ pause_play_stop = SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP
+ play_media = SUPPORT_PLAY_MEDIA
+ volume = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP
+
+ await async_setup_component(
+ hass,
+ MEDIA_DOMAIN,
+ {
+ MEDIA_DOMAIN: {
+ "platform": DOMAIN,
+ "entities": ["media_player.player_1", "media_player.player_2"],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ hass.states.async_set(
+ "media_player.player_1", STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}
+ )
+ await hass.async_block_till_done()
+ state = hass.states.get("media_player.media_group")
+ assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
+
+ hass.states.async_set(
+ "media_player.player_1",
+ STATE_ON,
+ {ATTR_SUPPORTED_FEATURES: pause_play_stop},
+ )
+ await hass.async_block_till_done()
+ state = hass.states.get("media_player.media_group")
+ assert state.attributes[ATTR_SUPPORTED_FEATURES] == pause_play_stop
+
+ hass.states.async_set(
+ "media_player.player_2",
+ STATE_OFF,
+ {ATTR_SUPPORTED_FEATURES: play_media | volume},
+ )
+ await hass.async_block_till_done()
+ state = hass.states.get("media_player.media_group")
+ assert (
+ state.attributes[ATTR_SUPPORTED_FEATURES]
+ == pause_play_stop | play_media | volume
+ )
+
+ hass.states.async_set(
+ "media_player.player_2", STATE_OFF, {ATTR_SUPPORTED_FEATURES: play_media}
+ )
+ await hass.async_block_till_done()
+ state = hass.states.get("media_player.media_group")
+ assert state.attributes[ATTR_SUPPORTED_FEATURES] == pause_play_stop | play_media
+
+
+async def test_service_calls(hass, mock_media_seek):
+ """Test service calls."""
+ await async_setup_component(
+ hass,
+ MEDIA_DOMAIN,
+ {
+ MEDIA_DOMAIN: [
+ {"platform": "demo"},
+ {
+ "platform": DOMAIN,
+ "entities": [
+ "media_player.bedroom",
+ "media_player.kitchen",
+ "media_player.living_room",
+ ],
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert hass.states.get("media_player.media_group").state == STATE_PLAYING
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "media_player.media_group"},
+ blocking=True,
+ )
+
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.bedroom").state == STATE_OFF
+ assert hass.states.get("media_player.kitchen").state == STATE_OFF
+ assert hass.states.get("media_player.living_room").state == STATE_OFF
+
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "media_player.media_group"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.bedroom").state == STATE_PLAYING
+ assert hass.states.get("media_player.kitchen").state == STATE_PLAYING
+ assert hass.states.get("media_player.living_room").state == STATE_PLAYING
+
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_MEDIA_PAUSE,
+ {ATTR_ENTITY_ID: "media_player.media_group"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.bedroom").state == STATE_PAUSED
+ assert hass.states.get("media_player.kitchen").state == STATE_PAUSED
+ assert hass.states.get("media_player.living_room").state == STATE_PAUSED
+
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_MEDIA_PLAY,
+ {ATTR_ENTITY_ID: "media_player.media_group"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.bedroom").state == STATE_PLAYING
+ assert hass.states.get("media_player.kitchen").state == STATE_PLAYING
+ assert hass.states.get("media_player.living_room").state == STATE_PLAYING
+
+ # ATTR_MEDIA_TRACK is not supported by bedroom and living_room players
+ assert hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_TRACK] == 1
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_MEDIA_NEXT_TRACK,
+ {ATTR_ENTITY_ID: "media_player.media_group"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_TRACK] == 2
+
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_MEDIA_PREVIOUS_TRACK,
+ {ATTR_ENTITY_ID: "media_player.media_group"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_TRACK] == 1
+
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_PLAY_MEDIA,
+ {
+ ATTR_ENTITY_ID: "media_player.media_group",
+ ATTR_MEDIA_CONTENT_TYPE: "some_type",
+ ATTR_MEDIA_CONTENT_ID: "some_id",
+ },
+ )
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_CONTENT_ID]
+ == "some_id"
+ )
+ # media_player.kitchen is skipped because it always returns "bounzz-1"
+ assert (
+ hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_CONTENT_ID]
+ == "some_id"
+ )
+
+ state = hass.states.get("media_player.media_group")
+ assert state.attributes[ATTR_SUPPORTED_FEATURES] & SUPPORT_SEEK
+ assert not mock_media_seek.called
+
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_MEDIA_SEEK,
+ {
+ ATTR_ENTITY_ID: "media_player.media_group",
+ ATTR_MEDIA_SEEK_POSITION: 100,
+ },
+ )
+ await hass.async_block_till_done()
+ assert mock_media_seek.called
+
+ assert (
+ hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_VOLUME_LEVEL] == 1
+ )
+ assert (
+ hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_VOLUME_LEVEL] == 1
+ )
+ assert (
+ hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_VOLUME_LEVEL]
+ == 1
+ )
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_VOLUME_SET,
+ {
+ ATTR_ENTITY_ID: "media_player.media_group",
+ ATTR_MEDIA_VOLUME_LEVEL: 0.5,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_VOLUME_LEVEL]
+ == 0.5
+ )
+ assert (
+ hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_VOLUME_LEVEL]
+ == 0.5
+ )
+ assert (
+ hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_VOLUME_LEVEL]
+ == 0.5
+ )
+
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_VOLUME_UP,
+ {ATTR_ENTITY_ID: "media_player.media_group"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_VOLUME_LEVEL]
+ == 0.6
+ )
+ assert (
+ hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_VOLUME_LEVEL]
+ == 0.6
+ )
+ assert (
+ hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_VOLUME_LEVEL]
+ == 0.6
+ )
+
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_VOLUME_DOWN,
+ {ATTR_ENTITY_ID: "media_player.media_group"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_VOLUME_LEVEL]
+ == 0.5
+ )
+ assert (
+ hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_VOLUME_LEVEL]
+ == 0.5
+ )
+ assert (
+ hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_VOLUME_LEVEL]
+ == 0.5
+ )
+
+ assert (
+ hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_VOLUME_MUTED]
+ is False
+ )
+ assert (
+ hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_VOLUME_MUTED]
+ is False
+ )
+ assert (
+ hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_VOLUME_MUTED]
+ is False
+ )
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_VOLUME_MUTE,
+ {ATTR_ENTITY_ID: "media_player.media_group", ATTR_MEDIA_VOLUME_MUTED: True},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_VOLUME_MUTED]
+ is True
+ )
+ assert (
+ hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_VOLUME_MUTED]
+ is True
+ )
+ assert (
+ hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_VOLUME_MUTED]
+ is True
+ )
+
+ assert (
+ hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_SHUFFLE] is False
+ )
+ assert (
+ hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_SHUFFLE] is False
+ )
+ assert (
+ hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_SHUFFLE]
+ is False
+ )
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_SHUFFLE_SET,
+ {ATTR_ENTITY_ID: "media_player.media_group", ATTR_MEDIA_SHUFFLE: True},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_SHUFFLE] is True
+ )
+ assert (
+ hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_SHUFFLE] is True
+ )
+ assert (
+ hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_SHUFFLE]
+ is True
+ )
+
+ assert hass.states.get("media_player.bedroom").state == STATE_PLAYING
+ assert hass.states.get("media_player.kitchen").state == STATE_PLAYING
+ assert hass.states.get("media_player.living_room").state == STATE_PLAYING
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_CLEAR_PLAYLIST,
+ {ATTR_ENTITY_ID: "media_player.media_group"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ # SERVICE_CLEAR_PLAYLIST is not supported by bedroom and living_room players
+ assert hass.states.get("media_player.kitchen").state == STATE_OFF
+
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_MEDIA_PLAY,
+ {ATTR_ENTITY_ID: "media_player.kitchen"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.bedroom").state == STATE_PLAYING
+ assert hass.states.get("media_player.kitchen").state == STATE_PLAYING
+ assert hass.states.get("media_player.living_room").state == STATE_PLAYING
+ await hass.services.async_call(
+ MEDIA_DOMAIN,
+ SERVICE_MEDIA_STOP,
+ {ATTR_ENTITY_ID: "media_player.media_group"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.bedroom").state == STATE_OFF
+ assert hass.states.get("media_player.kitchen").state == STATE_OFF
+ assert hass.states.get("media_player.living_room").state == STATE_OFF
+
+
+async def test_nested_group(hass):
+ """Test nested media group."""
+ hass.states.async_set("media_player.player_1", "on")
+ await async_setup_component(
+ hass,
+ MEDIA_DOMAIN,
+ {
+ MEDIA_DOMAIN: [
+ {
+ "platform": DOMAIN,
+ "entities": ["media_player.group_1"],
+ "name": "Nested Group",
+ },
+ {
+ "platform": DOMAIN,
+ "entities": ["media_player.player_1", "media_player.player_2"],
+ "name": "Group 1",
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("media_player.group_1")
+ assert state is not None
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ENTITY_ID) == [
+ "media_player.player_1",
+ "media_player.player_2",
+ ]
+
+ state = hass.states.get("media_player.nested_group")
+ assert state is not None
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ENTITY_ID) == ["media_player.group_1"]
diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py
index 5f7e4b7fbf5..97d4fb092fc 100644
--- a/tests/components/habitica/test_init.py
+++ b/tests/components/habitica/test_init.py
@@ -1,15 +1,33 @@
-"""Test the habitica init module."""
+"""Test the habitica module."""
+import pytest
+
from homeassistant.components.habitica.const import (
+ ATTR_ARGS,
+ ATTR_DATA,
+ ATTR_PATH,
DEFAULT_URL,
DOMAIN,
+ EVENT_API_CALL_SUCCESS,
SERVICE_API_CALL,
)
+from homeassistant.components.habitica.sensor import TASKS_TYPES
+from homeassistant.const import ATTR_NAME
-from tests.common import MockConfigEntry
+from tests.common import MockConfigEntry, async_capture_events
+
+TEST_API_CALL_ARGS = {"text": "Use API from Home Assistant", "type": "todo"}
+TEST_USER_NAME = "test_user"
-async def test_entry_setup_unload(hass, aioclient_mock):
- """Test integration setup and unload."""
+@pytest.fixture
+def capture_api_call_success(hass):
+ """Capture api_call events."""
+ return async_capture_events(hass, EVENT_API_CALL_SUCCESS)
+
+
+@pytest.fixture
+def habitica_entry(hass):
+ """Test entry for the following tests."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="test-api-user",
@@ -20,17 +38,96 @@ async def test_entry_setup_unload(hass, aioclient_mock):
},
)
entry.add_to_hass(hass)
+ return entry
+
+@pytest.fixture
+def common_requests(aioclient_mock):
+ """Register requests for the tests."""
aioclient_mock.get(
"https://habitica.com/api/v3/user",
- json={"data": {"api_user": "test-api-user", "profile": {"name": "test_user"}}},
+ json={
+ "data": {
+ "api_user": "test-api-user",
+ "profile": {"name": TEST_USER_NAME},
+ "stats": {
+ "class": "test-class",
+ "con": 1,
+ "exp": 2,
+ "gp": 3,
+ "hp": 4,
+ "int": 5,
+ "lvl": 6,
+ "maxHealth": 7,
+ "maxMP": 8,
+ "mp": 9,
+ "per": 10,
+ "points": 11,
+ "str": 12,
+ "toNextLevel": 13,
+ },
+ }
+ },
+ )
+ for n_tasks, task_type in enumerate(TASKS_TYPES.keys(), start=1):
+ aioclient_mock.get(
+ f"https://habitica.com/api/v3/tasks/user?type={task_type}",
+ json={
+ "data": [
+ {"text": f"this is a mock {task_type} #{task}", "id": f"{task}"}
+ for task in range(n_tasks)
+ ]
+ },
+ )
+
+ aioclient_mock.post(
+ "https://habitica.com/api/v3/tasks/user",
+ status=201,
+ json={"data": TEST_API_CALL_ARGS},
)
- assert await hass.config_entries.async_setup(entry.entry_id)
+ return aioclient_mock
+
+
+async def test_entry_setup_unload(hass, habitica_entry, common_requests):
+ """Test integration setup and unload."""
+ assert await hass.config_entries.async_setup(habitica_entry.entry_id)
await hass.async_block_till_done()
assert hass.services.has_service(DOMAIN, SERVICE_API_CALL)
- assert await hass.config_entries.async_unload(entry.entry_id)
+ assert await hass.config_entries.async_unload(habitica_entry.entry_id)
+
+ assert not hass.services.has_service(DOMAIN, SERVICE_API_CALL)
+
+
+async def test_service_call(
+ hass, habitica_entry, common_requests, capture_api_call_success
+):
+ """Test integration setup, service call and unload."""
+
+ assert await hass.config_entries.async_setup(habitica_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert hass.services.has_service(DOMAIN, SERVICE_API_CALL)
+
+ assert len(capture_api_call_success) == 0
+
+ TEST_SERVICE_DATA = {
+ ATTR_NAME: "test_user",
+ ATTR_PATH: ["tasks", "user", "post"],
+ ATTR_ARGS: TEST_API_CALL_ARGS,
+ }
+ assert await hass.services.async_call(
+ DOMAIN, SERVICE_API_CALL, TEST_SERVICE_DATA, blocking=True
+ )
+
+ assert len(capture_api_call_success) == 1
+ captured_data = capture_api_call_success[0].data
+ captured_data[ATTR_ARGS] = captured_data[ATTR_DATA]
+ del captured_data[ATTR_DATA]
+ assert captured_data == TEST_SERVICE_DATA
+
+ assert await hass.config_entries.async_unload(habitica_entry.entry_id)
assert not hass.services.has_service(DOMAIN, SERVICE_API_CALL)
diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py
index 7e9d7cd91c8..8377e5287d0 100644
--- a/tests/components/hassio/test_init.py
+++ b/tests/components/hassio/test_init.py
@@ -179,6 +179,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag
assert hassio_user.system_generated
assert len(hassio_user.groups) == 1
assert hassio_user.groups[0].id == GROUP_ID_ADMIN
+ assert hassio_user.name == "Supervisor"
for token in hassio_user.refresh_tokens.values():
if token.token == refresh_token:
break
@@ -206,6 +207,25 @@ async def test_setup_adds_admin_group_to_user(hass, aioclient_mock, hass_storage
assert user.is_admin
+async def test_setup_migrate_user_name(hass, aioclient_mock, hass_storage):
+ """Test setup with migrating the user name."""
+ # Create user with old name
+ user = await hass.auth.async_create_system_user("Hass.io")
+ await hass.auth.async_create_refresh_token(user)
+
+ hass_storage[STORAGE_KEY] = {
+ "data": {"hassio_user": user.id},
+ "key": STORAGE_KEY,
+ "version": 1,
+ }
+
+ with patch.dict(os.environ, MOCK_ENVIRON):
+ result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}})
+ assert result
+
+ assert user.name == "Supervisor"
+
+
async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage):
"""Test setup with API push default data."""
user = await hass.auth.async_create_system_user("Hass.io test")
diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py
index 2f69dc97a84..3e5b2aeaaed 100644
--- a/tests/components/here_travel_time/test_sensor.py
+++ b/tests/components/here_travel_time/test_sensor.py
@@ -132,7 +132,7 @@ def requests_mock_credentials_check(requests_mock):
@pytest.fixture
def requests_mock_truck_response(requests_mock_credentials_check):
- """Return a requests_mock for truck respones."""
+ """Return a requests_mock for truck response."""
modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_TRUCK, TRAFFIC_MODE_DISABLED]
response_url = _build_mock_url(
",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]),
@@ -147,7 +147,7 @@ def requests_mock_truck_response(requests_mock_credentials_check):
@pytest.fixture
def requests_mock_car_disabled_response(requests_mock_credentials_check):
- """Return a requests_mock for truck respones."""
+ """Return a requests_mock for truck response."""
modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED]
response_url = _build_mock_url(
",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]),
diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py
index bf8d34e6ffe..c4f85717cac 100644
--- a/tests/components/history/test_init.py
+++ b/tests/components/history/test_init.py
@@ -14,6 +14,7 @@ import homeassistant.core as ha
from homeassistant.helpers.json import JSONEncoder
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
@@ -829,23 +830,46 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state(hass, hass_clien
assert response_json[1][0]["entity_id"] == "light.cow"
-async def test_statistics_during_period(hass, hass_ws_client):
+POWER_SENSOR_ATTRIBUTES = {
+ "device_class": "power",
+ "state_class": "measurement",
+ "unit_of_measurement": "kW",
+}
+PRESSURE_SENSOR_ATTRIBUTES = {
+ "device_class": "pressure",
+ "state_class": "measurement",
+ "unit_of_measurement": "hPa",
+}
+TEMPERATURE_SENSOR_ATTRIBUTES = {
+ "device_class": "temperature",
+ "state_class": "measurement",
+ "unit_of_measurement": "°C",
+}
+
+
+@pytest.mark.parametrize(
+ "units, attributes, state, value",
+ [
+ (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000),
+ (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000),
+ (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50),
+ (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10),
+ (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312),
+ (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000),
+ ],
+)
+async def test_statistics_during_period(
+ hass, hass_ws_client, units, attributes, state, value
+):
"""Test statistics_during_period."""
now = dt_util.utcnow()
+ hass.config.units = units
await hass.async_add_executor_job(init_recorder_component, hass)
- await async_setup_component(
- hass,
- "history",
- {"history": {}},
- )
+ await async_setup_component(hass, "history", {})
await async_setup_component(hass, "sensor", {})
await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
- hass.states.async_set(
- "sensor.test",
- 10,
- attributes={"device_class": "temperature", "state_class": "measurement"},
- )
+ hass.states.async_set("sensor.test", state, attributes=attributes)
await hass.async_block_till_done()
await hass.async_add_executor_job(trigger_db_commit, hass)
@@ -861,12 +885,12 @@ async def test_statistics_during_period(hass, hass_ws_client):
"type": "history/statistics_during_period",
"start_time": now.isoformat(),
"end_time": now.isoformat(),
- "statistic_id": "sensor.test",
+ "statistic_ids": ["sensor.test"],
}
)
response = await client.receive_json()
assert response["success"]
- assert response["result"] == {"statistics": {}}
+ assert response["result"] == {}
client = await hass_ws_client()
await client.send_json(
@@ -874,26 +898,24 @@ async def test_statistics_during_period(hass, hass_ws_client):
"id": 1,
"type": "history/statistics_during_period",
"start_time": now.isoformat(),
- "statistic_id": "sensor.test",
+ "statistic_ids": ["sensor.test"],
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
- "statistics": {
- "sensor.test": [
- {
- "statistic_id": "sensor.test",
- "start": now.isoformat(),
- "mean": approx(10.0),
- "min": approx(10.0),
- "max": approx(10.0),
- "last_reset": None,
- "state": None,
- "sum": None,
- }
- ]
- }
+ "sensor.test": [
+ {
+ "statistic_id": "sensor.test",
+ "start": now.isoformat(),
+ "mean": approx(value),
+ "min": approx(value),
+ "max": approx(value),
+ "last_reset": None,
+ "state": None,
+ "sum": None,
+ }
+ ]
}
@@ -944,3 +966,71 @@ async def test_statistics_during_period_bad_end_time(hass, hass_ws_client):
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "invalid_end_time"
+
+
+@pytest.mark.parametrize(
+ "units, attributes, unit",
+ [
+ (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
+ (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
+ (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F"),
+ (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C"),
+ (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi"),
+ (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa"),
+ ],
+)
+async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit):
+ """Test list_statistic_ids."""
+ now = dt_util.utcnow()
+
+ hass.config.units = units
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(hass, "history", {"history": {}})
+ await async_setup_component(hass, "sensor", {})
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ hass.states.async_set("sensor.test", 10, attributes=attributes)
+ await hass.async_block_till_done()
+
+ await hass.async_add_executor_job(trigger_db_commit, hass)
+ await hass.async_block_till_done()
+
+ client = await hass_ws_client()
+ await client.send_json({"id": 1, "type": "history/list_statistic_ids"})
+ response = await client.receive_json()
+ assert response["success"]
+ assert response["result"] == []
+
+ hass.data[recorder.DATA_INSTANCE].do_adhoc_statistics(period="hourly", start=now)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ await client.send_json({"id": 2, "type": "history/list_statistic_ids"})
+ response = await client.receive_json()
+ assert response["success"]
+ assert response["result"] == [
+ {"statistic_id": "sensor.test", "unit_of_measurement": unit}
+ ]
+
+ await client.send_json(
+ {"id": 3, "type": "history/list_statistic_ids", "statistic_type": "dogs"}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ assert response["result"] == [
+ {"statistic_id": "sensor.test", "unit_of_measurement": unit}
+ ]
+
+ await client.send_json(
+ {"id": 4, "type": "history/list_statistic_ids", "statistic_type": "mean"}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ assert response["result"] == [
+ {"statistic_id": "sensor.test", "unit_of_measurement": unit}
+ ]
+
+ await client.send_json(
+ {"id": 5, "type": "history/list_statistic_ids", "statistic_type": "sum"}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ assert response["result"] == []
diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py
index 38372d09825..0e71594937f 100644
--- a/tests/components/homeassistant/triggers/test_numeric_state.py
+++ b/tests/components/homeassistant/triggers/test_numeric_state.py
@@ -47,9 +47,13 @@ async def setup_comp(hass):
}
},
)
+ hass.states.async_set("number.value_10", 10)
+ hass.states.async_set("sensor.value_10", 10)
-@pytest.mark.parametrize("below", (10, "input_number.value_10"))
+@pytest.mark.parametrize(
+ "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10")
+)
async def test_if_not_fires_on_entity_removal(hass, calls, below):
"""Test the firing with removed entity."""
hass.states.async_set("test.entity", 11)
@@ -75,7 +79,9 @@ async def test_if_not_fires_on_entity_removal(hass, calls, below):
assert len(calls) == 0
-@pytest.mark.parametrize("below", (10, "input_number.value_10"))
+@pytest.mark.parametrize(
+ "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10")
+)
async def test_if_fires_on_entity_change_below(hass, calls, below):
"""Test the firing with changed entity."""
hass.states.async_set("test.entity", 11)
@@ -120,7 +126,9 @@ async def test_if_fires_on_entity_change_below(hass, calls, below):
assert calls[0].data["id"] == 0
-@pytest.mark.parametrize("below", (10, "input_number.value_10"))
+@pytest.mark.parametrize(
+ "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10")
+)
async def test_if_fires_on_entity_change_over_to_below(hass, calls, below):
"""Test the firing with changed entity."""
hass.states.async_set("test.entity", 11)
@@ -147,7 +155,9 @@ async def test_if_fires_on_entity_change_over_to_below(hass, calls, below):
assert len(calls) == 1
-@pytest.mark.parametrize("below", (10, "input_number.value_10"))
+@pytest.mark.parametrize(
+ "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10")
+)
async def test_if_fires_on_entities_change_over_to_below(hass, calls, below):
"""Test the firing with changed entities."""
hass.states.async_set("test.entity_1", 11)
@@ -178,7 +188,9 @@ async def test_if_fires_on_entities_change_over_to_below(hass, calls, below):
assert len(calls) == 2
-@pytest.mark.parametrize("below", (10, "input_number.value_10"))
+@pytest.mark.parametrize(
+ "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10")
+)
async def test_if_not_fires_on_entity_change_below_to_below(hass, calls, below):
"""Test the firing with changed entity."""
context = Context()
@@ -217,7 +229,9 @@ async def test_if_not_fires_on_entity_change_below_to_below(hass, calls, below):
assert len(calls) == 1
-@pytest.mark.parametrize("below", (10, "input_number.value_10"))
+@pytest.mark.parametrize(
+ "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10")
+)
async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls, below):
"""Test the firing with changed entity."""
hass.states.async_set("test.entity", 11)
@@ -244,7 +258,9 @@ async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls, below):
assert len(calls) == 0
-@pytest.mark.parametrize("below", (10, "input_number.value_10"))
+@pytest.mark.parametrize(
+ "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10")
+)
async def test_if_not_fires_on_initial_entity_below(hass, calls, below):
"""Test the firing when starting with a match."""
hass.states.async_set("test.entity", 9)
@@ -271,7 +287,9 @@ async def test_if_not_fires_on_initial_entity_below(hass, calls, below):
assert len(calls) == 0
-@pytest.mark.parametrize("above", (10, "input_number.value_10"))
+@pytest.mark.parametrize(
+ "above", (10, "input_number.value_10", "number.value_10", "sensor.value_10")
+)
async def test_if_not_fires_on_initial_entity_above(hass, calls, above):
"""Test the firing when starting with a match."""
hass.states.async_set("test.entity", 11)
@@ -298,7 +316,9 @@ async def test_if_not_fires_on_initial_entity_above(hass, calls, above):
assert len(calls) == 0
-@pytest.mark.parametrize("above", (10, "input_number.value_10"))
+@pytest.mark.parametrize(
+ "above", (10, "input_number.value_10", "number.value_10", "sensor.value_10")
+)
async def test_if_fires_on_entity_change_above(hass, calls, above):
"""Test the firing with changed entity."""
hass.states.async_set("test.entity", 9)
@@ -1632,8 +1652,8 @@ def test_below_above():
)
-def test_schema_input_number():
- """Test input_number only is accepted for above/below."""
+def test_schema_unacceptable_entities():
+ """Test input_number, number & sensor only is accepted for above/below."""
with pytest.raises(vol.Invalid):
numeric_state_trigger.TRIGGER_SCHEMA(
{
diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py
index afaa9ea0892..84ed61322a2 100644
--- a/tests/components/homekit/test_accessories.py
+++ b/tests/components/homekit/test_accessories.py
@@ -13,7 +13,7 @@ from homeassistant.components.homekit.accessories import (
)
from homeassistant.components.homekit.const import (
ATTR_DISPLAY_NAME,
- ATTR_INTERGRATION,
+ ATTR_INTEGRATION,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_SOFTWARE_VERSION,
@@ -106,7 +106,7 @@ async def test_home_accessory(hass, hk_driver):
ATTR_MODEL: "Awesome",
ATTR_MANUFACTURER: "Lux Brands",
ATTR_SOFTWARE_VERSION: "0.4.3",
- ATTR_INTERGRATION: "luxe",
+ ATTR_INTEGRATION: "luxe",
},
)
assert acc3.available is False
diff --git a/tests/components/homekit/test_aidmanager.py b/tests/components/homekit/test_aidmanager.py
index df1bb14dd9e..dd9daaac43d 100644
--- a/tests/components/homekit/test_aidmanager.py
+++ b/tests/components/homekit/test_aidmanager.py
@@ -77,7 +77,7 @@ async def test_aid_generation(hass, device_reg, entity_reg):
aid_storage.delete_aid(get_system_unique_id(light_ent))
aid_storage.delete_aid(get_system_unique_id(light_ent2))
aid_storage.delete_aid(get_system_unique_id(remote_ent))
- aid_storage.delete_aid("non-existant-one")
+ aid_storage.delete_aid("non-existent-one")
for _ in range(0, 2):
assert (
diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py
index 1c68ae7d001..491d686162d 100644
--- a/tests/components/homekit/test_get_accessories.py
+++ b/tests/components/homekit/test_get_accessories.py
@@ -7,7 +7,7 @@ import homeassistant.components.climate as climate
import homeassistant.components.cover as cover
from homeassistant.components.homekit.accessories import TYPES, get_accessory
from homeassistant.components.homekit.const import (
- ATTR_INTERGRATION,
+ ATTR_INTEGRATION,
CONF_FEATURE_LIST,
FEATURE_ON_OFF,
TYPE_FAUCET,
@@ -66,7 +66,7 @@ def test_customize_options(config, name):
"""Test with customized options."""
mock_type = Mock()
conf = config.copy()
- conf[ATTR_INTERGRATION] = "platform_name"
+ conf[ATTR_INTEGRATION] = "platform_name"
with patch.dict(TYPES, {"Light": mock_type}):
entity_state = State("light.demo", "on")
get_accessory(None, None, entity_state, 2, conf)
diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py
index bd7af3b3596..5e9ea4fd4b6 100644
--- a/tests/components/homekit/test_homekit.py
+++ b/tests/components/homekit/test_homekit.py
@@ -54,7 +54,15 @@ from homeassistant.const import (
)
from homeassistant.core import State
from homeassistant.helpers import device_registry
-from homeassistant.helpers.entityfilter import generate_filter
+from homeassistant.helpers.entityfilter import (
+ CONF_EXCLUDE_DOMAINS,
+ CONF_EXCLUDE_ENTITIES,
+ CONF_EXCLUDE_ENTITY_GLOBS,
+ CONF_INCLUDE_DOMAINS,
+ CONF_INCLUDE_ENTITIES,
+ CONF_INCLUDE_ENTITY_GLOBS,
+ convert_filter,
+)
from homeassistant.setup import async_setup_component
from homeassistant.util import json as json_util
@@ -65,6 +73,27 @@ from tests.common import MockConfigEntry, mock_device_registry, mock_registry
IP_ADDRESS = "127.0.0.1"
+def generate_filter(
+ include_domains,
+ include_entities,
+ exclude_domains,
+ exclude_entites,
+ include_globs=None,
+ exclude_globs=None,
+):
+ """Generate an entity filter using the standard method."""
+ return convert_filter(
+ {
+ CONF_INCLUDE_DOMAINS: include_domains,
+ CONF_INCLUDE_ENTITIES: include_entities,
+ CONF_EXCLUDE_DOMAINS: exclude_domains,
+ CONF_EXCLUDE_ENTITIES: exclude_entites,
+ CONF_INCLUDE_ENTITY_GLOBS: include_globs or [],
+ CONF_EXCLUDE_ENTITY_GLOBS: exclude_globs or [],
+ }
+ )
+
+
@pytest.fixture(autouse=True)
def always_patch_driver(hk_driver):
"""Load the hk_driver fixture."""
@@ -1173,6 +1202,31 @@ async def test_homekit_start_in_accessory_mode(
assert homekit.status == STATUS_RUNNING
+async def test_homekit_start_in_accessory_mode_missing_entity(
+ hass, hk_driver, mock_zeroconf, device_reg, caplog
+):
+ """Test HomeKit start method in accessory mode when entity is not available."""
+ entry = await async_init_integration(hass)
+
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY)
+
+ homekit.bridge = Mock()
+ homekit.bridge.accessories = []
+ homekit.driver = hk_driver
+ homekit.driver.accessory = Accessory(hk_driver, "any")
+
+ with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch(
+ f"{PATH_HOMEKIT}.show_setup_message"
+ ), patch("pyhap.accessory_driver.AccessoryDriver.async_start"):
+ await homekit.async_start()
+
+ await hass.async_block_till_done()
+ mock_add_acc.assert_not_called()
+ assert homekit.status == STATUS_WAIT
+
+ assert "entity not available" in caplog.text
+
+
async def test_wait_for_port_to_free(hass, hk_driver, mock_zeroconf, caplog):
"""Test we wait for the port to free before declaring unload success."""
await async_setup_component(hass, "persistent_notification", {})
diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py
index cc6c7ae4b9f..96dbd3b0718 100644
--- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py
+++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py
@@ -59,6 +59,9 @@ async def test_ecobee3_setup(hass):
assert climate_state.attributes["min_humidity"] == 20
assert climate_state.attributes["max_humidity"] == 50
+ climate_sensor = entity_registry.async_get("sensor.homew_current_temperature")
+ assert climate_sensor.unique_id == "homekit-123456789012-aid:1-sid:16-cid:16"
+
occ1 = entity_registry.async_get("binary_sensor.kitchen")
assert occ1.unique_id == "homekit-AB1C-56"
diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py
index bc9fdaa1013..07a5025ac88 100644
--- a/tests/components/homekit_controller/test_climate.py
+++ b/tests/components/homekit_controller/test_climate.py
@@ -1,6 +1,4 @@
"""Basic checks for HomeKitclimate."""
-from unittest.mock import patch
-
from aiohomekit.model.characteristics import (
ActivationStateValues,
CharacteristicsTypes,
@@ -21,7 +19,6 @@ from homeassistant.components.climate.const import (
SERVICE_SET_SWING_MODE,
SERVICE_SET_TEMPERATURE,
)
-from homeassistant.const import TEMP_FAHRENHEIT
from tests.components.homekit_controller.common import setup_test_component
@@ -274,7 +271,6 @@ async def test_climate_cannot_set_thermostat_temp_range_in_wrong_mode(hass, utcn
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.testdevice",
- "hvac_mode": HVAC_MODE_HEAT_COOL,
"temperature": 22,
"target_temp_low": 20,
"target_temp_high": 24,
@@ -373,6 +369,35 @@ async def test_climate_set_thermostat_temp_on_sspa_device(hass, utcnow):
)
assert helper.characteristics[TEMPERATURE_TARGET].value == 21
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {
+ "entity_id": "climate.testdevice",
+ "temperature": 22,
+ },
+ blocking=True,
+ )
+ assert helper.characteristics[TEMPERATURE_TARGET].value == 22
+
+
+async def test_climate_set_mode_via_temp(hass, utcnow):
+ """Test setting temperature and mode at same tims."""
+ helper = await setup_test_component(hass, create_thermostat_single_set_point_auto)
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {
+ "entity_id": "climate.testdevice",
+ "temperature": 21,
+ "hvac_mode": HVAC_MODE_HEAT,
+ },
+ blocking=True,
+ )
+ assert helper.characteristics[TEMPERATURE_TARGET].value == 21
+ assert helper.characteristics[HEATING_COOLING_TARGET].value == 1
+
await hass.services.async_call(
DOMAIN,
SERVICE_SET_TEMPERATURE,
@@ -384,6 +409,7 @@ async def test_climate_set_thermostat_temp_on_sspa_device(hass, utcnow):
blocking=True,
)
assert helper.characteristics[TEMPERATURE_TARGET].value == 22
+ assert helper.characteristics[HEATING_COOLING_TARGET].value == 3
async def test_climate_change_thermostat_humidity(hass, utcnow):
@@ -448,11 +474,6 @@ async def test_climate_read_thermostat_state(hass, utcnow):
state = await helper.poll_and_get_state()
assert state.state == HVAC_MODE_HEAT_COOL
- # Ensure converted Fahrenheit precision is reported in tenths
- with patch.object(hass.config.units, "temperature_unit", TEMP_FAHRENHEIT):
- state = await helper.poll_and_get_state()
- assert state.attributes["current_temperature"] == 69.8
-
async def test_hvac_mode_vs_hvac_action(hass, utcnow):
"""Check that we haven't conflated hvac_mode and hvac_action."""
diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py
index 99c6966e827..52685334500 100644
--- a/tests/components/homekit_controller/test_config_flow.py
+++ b/tests/components/homekit_controller/test_config_flow.py
@@ -1,7 +1,7 @@
"""Tests for homekit_controller config flow."""
from unittest import mock
import unittest.mock
-from unittest.mock import patch
+from unittest.mock import AsyncMock, patch
import aiohomekit
from aiohomekit.model import Accessories, Accessory
@@ -11,6 +11,7 @@ import pytest
from homeassistant import config_entries
from homeassistant.components.homekit_controller import config_flow
+from homeassistant.components.homekit_controller.const import KNOWN_DEVICES
from homeassistant.helpers import device_registry
from tests.common import MockConfigEntry, mock_device_registry
@@ -383,11 +384,16 @@ async def test_discovery_invalid_config_entry(hass, controller):
async def test_discovery_already_configured(hass, controller):
"""Already configured."""
- MockConfigEntry(
+ entry = MockConfigEntry(
domain="homekit_controller",
- data={"AccessoryPairingID": "00:00:00:00:00:00"},
+ data={
+ "AccessoryIP": "4.4.4.4",
+ "AccessoryPort": 66,
+ "AccessoryPairingID": "00:00:00:00:00:00",
+ },
unique_id="00:00:00:00:00:00",
- ).add_to_hass(hass)
+ )
+ entry.add_to_hass(hass)
device = setup_mock_accessory(controller)
discovery_info = get_device_discovery_info(device)
@@ -403,6 +409,49 @@ async def test_discovery_already_configured(hass, controller):
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
+ assert entry.data["AccessoryIP"] == discovery_info["host"]
+ assert entry.data["AccessoryPort"] == discovery_info["port"]
+
+
+async def test_discovery_already_configured_update_csharp(hass, controller):
+ """Already configured and csharp changes."""
+ entry = MockConfigEntry(
+ domain="homekit_controller",
+ data={
+ "AccessoryIP": "4.4.4.4",
+ "AccessoryPort": 66,
+ "AccessoryPairingID": "AA:BB:CC:DD:EE:FF",
+ },
+ unique_id="aa:bb:cc:dd:ee:ff",
+ )
+ entry.add_to_hass(hass)
+
+ connection_mock = AsyncMock()
+ connection_mock.pairing.connect.reconnect_soon = AsyncMock()
+ connection_mock.async_refresh_entity_map = AsyncMock()
+ hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock}
+
+ device = setup_mock_accessory(controller)
+ discovery_info = get_device_discovery_info(device)
+
+ # Set device as already paired
+ discovery_info["properties"]["sf"] = 0x00
+ discovery_info["properties"]["c#"] = 99999
+ discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF"
+
+ # Device is discovered
+ result = await hass.config_entries.flow.async_init(
+ "homekit_controller",
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ data=discovery_info,
+ )
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+ await hass.async_block_till_done()
+
+ assert entry.data["AccessoryIP"] == discovery_info["host"]
+ assert entry.data["AccessoryPort"] == discovery_info["port"]
+ assert connection_mock.async_refresh_entity_map.await_count == 1
@pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS)
diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py
index 4b7a3421b0a..400a1f32729 100644
--- a/tests/components/http/test_forwarded.py
+++ b/tests/components/http/test_forwarded.py
@@ -33,9 +33,9 @@ async def test_x_forwarded_for_without_trusted_proxy(aiohttp_client, caplog):
mock_api_client = await aiohttp_client(app)
resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"})
- assert resp.status == 200
+ assert resp.status == 400
assert (
- "Received X-Forwarded-For header from untrusted proxy 127.0.0.1, headers not processed"
+ "Received X-Forwarded-For header from an untrusted proxy 127.0.0.1"
in caplog.text
)
@@ -103,35 +103,13 @@ async def test_x_forwarded_for_disabled_with_proxy(aiohttp_client, caplog):
mock_api_client = await aiohttp_client(app)
resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"})
- assert resp.status == 200
+ assert resp.status == 400
assert (
"A request from a reverse proxy was received from 127.0.0.1, but your HTTP "
"integration is not set-up for reverse proxies" in caplog.text
)
-async def test_x_forwarded_for_with_untrusted_proxy(aiohttp_client):
- """Test that we get the IP from transport with untrusted proxy."""
-
- async def handler(request):
- url = mock_api_client.make_url("/")
- assert request.host == f"{url.host}:{url.port}"
- assert request.scheme == "http"
- assert not request.secure
- assert request.remote == "127.0.0.1"
-
- return web.Response()
-
- app = web.Application()
- app.router.add_get("/", handler)
- async_setup_forwarded(app, True, [ip_network("1.1.1.1")])
-
- mock_api_client = await aiohttp_client(app)
- resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"})
-
- assert resp.status == 200
-
-
async def test_x_forwarded_for_with_spoofed_header(aiohttp_client):
"""Test that we get the IP from the transport with a spoofed header."""
@@ -205,31 +183,6 @@ async def test_x_forwarded_for_with_multiple_headers(aiohttp_client, caplog):
assert "Too many headers for X-Forwarded-For" in caplog.text
-async def test_x_forwarded_proto_without_trusted_proxy(aiohttp_client):
- """Test that proto header is ignored when untrusted."""
-
- async def handler(request):
- url = mock_api_client.make_url("/")
- assert request.host == f"{url.host}:{url.port}"
- assert request.scheme == "http"
- assert not request.secure
- assert request.remote == "127.0.0.1"
-
- return web.Response()
-
- app = web.Application()
- app.router.add_get("/", handler)
-
- async_setup_forwarded(app, True, [])
-
- mock_api_client = await aiohttp_client(app)
- resp = await mock_api_client.get(
- "/", headers={X_FORWARDED_FOR: "255.255.255.255", X_FORWARDED_PROTO: "https"}
- )
-
- assert resp.status == 200
-
-
@pytest.mark.parametrize(
"x_forwarded_for,remote,x_forwarded_proto,secure",
[
@@ -409,32 +362,6 @@ async def test_x_forwarded_proto_incorrect_number_of_elements(
)
-async def test_x_forwarded_host_without_trusted_proxy(aiohttp_client):
- """Test that host header is ignored when untrusted."""
-
- async def handler(request):
- url = mock_api_client.make_url("/")
- assert request.host == f"{url.host}:{url.port}"
- assert request.scheme == "http"
- assert not request.secure
- assert request.remote == "127.0.0.1"
-
- return web.Response()
-
- app = web.Application()
- app.router.add_get("/", handler)
-
- async_setup_forwarded(app, True, [])
-
- mock_api_client = await aiohttp_client(app)
- resp = await mock_api_client.get(
- "/",
- headers={X_FORWARDED_FOR: "255.255.255.255", X_FORWARDED_HOST: "example.com"},
- )
-
- assert resp.status == 200
-
-
async def test_x_forwarded_host_with_trusted_proxy(aiohttp_client):
"""Test that we get the host header if proxy is trusted."""
diff --git a/tests/components/humidifier/test_device_action.py b/tests/components/humidifier/test_device_action.py
index 1bf1c110ec6..39767b569ac 100644
--- a/tests/components/humidifier/test_device_action.py
+++ b/tests/components/humidifier/test_device_action.py
@@ -31,7 +31,24 @@ def entity_reg(hass):
return mock_registry(hass)
-async def test_get_actions(hass, device_reg, entity_reg):
+@pytest.mark.parametrize(
+ "set_state,features_reg,features_state,expected_action_types",
+ [
+ (False, 0, 0, []),
+ (False, const.SUPPORT_MODES, 0, ["set_mode"]),
+ (True, 0, 0, []),
+ (True, 0, const.SUPPORT_MODES, ["set_mode"]),
+ ],
+)
+async def test_get_actions(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ features_reg,
+ features_state,
+ expected_action_types,
+):
"""Test we get the expected actions from a humidifier."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@@ -39,124 +56,36 @@ async def test_get_actions(hass, device_reg, entity_reg):
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- hass.states.async_set("humidifier.test_5678", STATE_ON, {})
- hass.states.async_set(
- "humidifier.test_5678", "attributes", {"supported_features": 1}
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ supported_features=features_reg,
)
- expected_actions = [
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state}
+ )
+ expected_actions = []
+ basic_action_types = ["turn_on", "turn_off", "toggle", "set_humidity"]
+ expected_actions += [
{
"domain": DOMAIN,
- "type": "turn_on",
+ "type": action,
"device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "turn_off",
- "device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "toggle",
- "device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "set_humidity",
- "device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "set_mode",
- "device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
+ "entity_id": f"{DOMAIN}.test_5678",
+ }
+ for action in basic_action_types
]
- actions = await async_get_device_automations(hass, "action", device_entry.id)
- assert_lists_same(actions, expected_actions)
-
-
-async def test_get_action_no_modes(hass, device_reg, entity_reg):
- """Test we get the expected actions from a humidifier."""
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- hass.states.async_set("humidifier.test_5678", STATE_ON, {})
- hass.states.async_set(
- "humidifier.test_5678", "attributes", {"supported_features": 0}
- )
- expected_actions = [
+ expected_actions += [
{
"domain": DOMAIN,
- "type": "turn_on",
+ "type": action,
"device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "turn_off",
- "device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "toggle",
- "device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "set_humidity",
- "device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
- ]
- actions = await async_get_device_automations(hass, "action", device_entry.id)
- assert_lists_same(actions, expected_actions)
-
-
-async def test_get_action_no_state(hass, device_reg, entity_reg):
- """Test we get the expected actions from a humidifier."""
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- expected_actions = [
- {
- "domain": DOMAIN,
- "type": "turn_on",
- "device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "turn_off",
- "device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "toggle",
- "device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
- {
- "domain": DOMAIN,
- "type": "set_humidity",
- "device_id": device_entry.id,
- "entity_id": "humidifier.test_5678",
- },
+ "entity_id": f"{DOMAIN}.test_5678",
+ }
+ for action in expected_action_types
]
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert_lists_same(actions, expected_actions)
@@ -291,69 +220,181 @@ async def test_action(hass):
assert len(toggle_calls) == 1
-async def test_capabilities(hass):
+@pytest.mark.parametrize(
+ "set_state,capabilities_reg,capabilities_state,action,expected_capabilities",
+ [
+ (
+ False,
+ {},
+ {},
+ "set_humidity",
+ [
+ {
+ "name": "humidity",
+ "required": True,
+ "type": "integer",
+ }
+ ],
+ ),
+ (
+ False,
+ {},
+ {},
+ "set_mode",
+ [
+ {
+ "name": "mode",
+ "options": [],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ False,
+ {const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY]},
+ {},
+ "set_mode",
+ [
+ {
+ "name": "mode",
+ "options": [("home", "home"), ("away", "away")],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ True,
+ {},
+ {},
+ "set_humidity",
+ [
+ {
+ "name": "humidity",
+ "required": True,
+ "type": "integer",
+ }
+ ],
+ ),
+ (
+ True,
+ {},
+ {},
+ "set_mode",
+ [
+ {
+ "name": "mode",
+ "options": [],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ True,
+ {},
+ {const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY]},
+ "set_mode",
+ [
+ {
+ "name": "mode",
+ "options": [("home", "home"), ("away", "away")],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ ],
+)
+async def test_capabilities(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ capabilities_reg,
+ capabilities_state,
+ action,
+ expected_capabilities,
+):
"""Test getting capabilities."""
- # Test capabililities without state
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ capabilities=capabilities_reg,
+ )
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678",
+ STATE_ON,
+ capabilities_state,
+ )
+
capabilities = await device_action.async_get_action_capabilities(
hass,
{
"domain": DOMAIN,
"device_id": "abcdefgh",
- "entity_id": "humidifier.entity",
- "type": "set_mode",
+ "entity_id": f"{DOMAIN}.test_5678",
+ "type": action,
},
)
assert capabilities and "extra_fields" in capabilities
- assert voluptuous_serialize.convert(
- capabilities["extra_fields"], custom_serializer=cv.custom_serializer
- ) == [{"name": "mode", "options": [], "required": True, "type": "select"}]
-
- # Set state
- hass.states.async_set(
- "humidifier.entity",
- STATE_ON,
- {const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY]},
+ assert (
+ voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ )
+ == expected_capabilities
)
- # Set humidity
+
+@pytest.mark.parametrize(
+ "action,capability_name,extra",
+ [
+ ("set_humidity", "humidity", {"type": "integer"}),
+ ("set_mode", "mode", {"type": "select", "options": []}),
+ ],
+)
+async def test_capabilities_missing_entity(
+ hass, device_reg, entity_reg, action, capability_name, extra
+):
+ """Test getting capabilities."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+
capabilities = await device_action.async_get_action_capabilities(
hass,
{
"domain": DOMAIN,
"device_id": "abcdefgh",
- "entity_id": "humidifier.entity",
- "type": "set_humidity",
+ "entity_id": f"{DOMAIN}.test_5678",
+ "type": action,
},
)
- assert capabilities and "extra_fields" in capabilities
-
- assert voluptuous_serialize.convert(
- capabilities["extra_fields"], custom_serializer=cv.custom_serializer
- ) == [{"name": "humidity", "required": True, "type": "integer"}]
-
- # Set mode
- capabilities = await device_action.async_get_action_capabilities(
- hass,
+ expected_capabilities = [
{
- "domain": DOMAIN,
- "device_id": "abcdefgh",
- "entity_id": "humidifier.entity",
- "type": "set_mode",
- },
- )
-
- assert capabilities and "extra_fields" in capabilities
-
- assert voluptuous_serialize.convert(
- capabilities["extra_fields"], custom_serializer=cv.custom_serializer
- ) == [
- {
- "name": "mode",
- "options": [("home", "home"), ("away", "away")],
+ "name": capability_name,
"required": True,
- "type": "select",
+ **extra,
}
]
+
+ assert capabilities and "extra_fields" in capabilities
+
+ assert (
+ voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ )
+ == expected_capabilities
+ )
diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py
index 59887f65a33..0d0f65d2c97 100644
--- a/tests/components/humidifier/test_device_condition.py
+++ b/tests/components/humidifier/test_device_condition.py
@@ -11,7 +11,6 @@ from homeassistant.setup import async_setup_component
from tests.common import (
MockConfigEntry,
assert_lists_same,
- async_get_device_automation_capabilities,
async_get_device_automations,
async_mock_service,
mock_device_registry,
@@ -38,7 +37,24 @@ def calls(hass):
return async_mock_service(hass, "test", "automation")
-async def test_get_conditions(hass, device_reg, entity_reg):
+@pytest.mark.parametrize(
+ "set_state,features_reg,features_state,expected_condition_types",
+ [
+ (False, 0, 0, []),
+ (False, const.SUPPORT_MODES, 0, ["is_mode"]),
+ (True, 0, 0, []),
+ (True, 0, const.SUPPORT_MODES, ["is_mode"]),
+ ],
+)
+async def test_get_conditions(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ features_reg,
+ features_state,
+ expected_condition_types,
+):
"""Test we get the expected conditions from a humidifier."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@@ -46,80 +62,38 @@ async def test_get_conditions(hass, device_reg, entity_reg):
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- hass.states.async_set(
- f"{DOMAIN}.test_5678",
- STATE_ON,
- {
- ATTR_MODE: const.MODE_AWAY,
- const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY],
- },
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ supported_features=features_reg,
)
- hass.states.async_set(
- "humidifier.test_5678", "attributes", {"supported_features": 1}
- )
- expected_conditions = [
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state}
+ )
+ expected_conditions = []
+ basic_condition_types = ["is_on", "is_off"]
+ expected_conditions += [
{
"condition": "device",
"domain": DOMAIN,
- "type": "is_off",
+ "type": condition,
"device_id": device_entry.id,
"entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_on",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_mode",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
+ }
+ for condition in basic_condition_types
]
- conditions = await async_get_device_automations(hass, "condition", device_entry.id)
- assert_lists_same(conditions, expected_conditions)
-
-
-async def test_get_conditions_toggle_only(hass, device_reg, entity_reg):
- """Test we get the expected conditions from a humidifier."""
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- hass.states.async_set(
- f"{DOMAIN}.test_5678",
- STATE_ON,
- {
- ATTR_MODE: const.MODE_AWAY,
- const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY],
- },
- )
- hass.states.async_set(
- "humidifier.test_5678", "attributes", {"supported_features": 0}
- )
- expected_conditions = [
+ expected_conditions += [
{
"condition": "device",
"domain": DOMAIN,
- "type": "is_off",
+ "type": condition,
"device_id": device_entry.id,
"entity_id": f"{DOMAIN}.test_5678",
- },
- {
- "condition": "device",
- "domain": DOMAIN,
- "type": "is_on",
- "device_id": device_entry.id,
- "entity_id": f"{DOMAIN}.test_5678",
- },
+ }
+ for condition in expected_condition_types
]
conditions = await async_get_device_automations(hass, "condition", device_entry.id)
assert_lists_same(conditions, expected_conditions)
@@ -227,81 +201,206 @@ async def test_if_state(hass, calls):
assert len(calls) == 3
-async def test_capabilities(hass):
- """Test capabilities."""
- hass.states.async_set(
- "humidifier.entity",
- STATE_ON,
- {
- ATTR_MODE: const.MODE_AWAY,
- const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY],
- },
- )
-
- # Test mode
- capabilities = await device_condition.async_get_condition_capabilities(
- hass,
- {
- "condition": "device",
- "domain": DOMAIN,
- "device_id": "",
- "entity_id": "humidifier.entity",
- "type": "is_mode",
- },
- )
-
- assert capabilities and "extra_fields" in capabilities
-
- assert voluptuous_serialize.convert(
- capabilities["extra_fields"], custom_serializer=cv.custom_serializer
- ) == [
- {
- "name": "mode",
- "options": [("home", "home"), ("away", "away")],
- "required": True,
- "type": "select",
- }
- ]
-
-
-async def test_capabilities_no_state(hass):
- """Test capabilities while state not available."""
- # Test mode
- capabilities = await device_condition.async_get_condition_capabilities(
- hass,
- {
- "condition": "device",
- "domain": DOMAIN,
- "device_id": "",
- "entity_id": "humidifier.entity",
- "type": "is_mode",
- },
- )
-
- assert capabilities and "extra_fields" in capabilities
-
- assert voluptuous_serialize.convert(
- capabilities["extra_fields"], custom_serializer=cv.custom_serializer
- ) == [{"name": "mode", "options": [], "required": True, "type": "select"}]
-
-
-async def test_get_condition_capabilities(hass, device_reg, entity_reg):
- """Test we get the expected toggle capabilities."""
+@pytest.mark.parametrize(
+ "set_state,capabilities_reg,capabilities_state,condition,expected_capabilities",
+ [
+ (
+ False,
+ {},
+ {},
+ "is_mode",
+ [
+ {
+ "name": "mode",
+ "options": [],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ False,
+ {const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY]},
+ {},
+ "is_mode",
+ [
+ {
+ "name": "mode",
+ "options": [("home", "home"), ("away", "away")],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ False,
+ {},
+ {},
+ "is_off",
+ [
+ {
+ "name": "for",
+ "optional": True,
+ "type": "positive_time_period_dict",
+ }
+ ],
+ ),
+ (
+ False,
+ {},
+ {},
+ "is_on",
+ [
+ {
+ "name": "for",
+ "optional": True,
+ "type": "positive_time_period_dict",
+ }
+ ],
+ ),
+ (
+ True,
+ {},
+ {},
+ "is_mode",
+ [
+ {
+ "name": "mode",
+ "options": [],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ True,
+ {},
+ {const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY]},
+ "is_mode",
+ [
+ {
+ "name": "mode",
+ "options": [("home", "home"), ("away", "away")],
+ "required": True,
+ "type": "select",
+ }
+ ],
+ ),
+ (
+ True,
+ {},
+ {},
+ "is_off",
+ [
+ {
+ "name": "for",
+ "optional": True,
+ "type": "positive_time_period_dict",
+ }
+ ],
+ ),
+ (
+ True,
+ {},
+ {},
+ "is_on",
+ [
+ {
+ "name": "for",
+ "optional": True,
+ "type": "positive_time_period_dict",
+ }
+ ],
+ ),
+ ],
+)
+async def test_capabilities(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ capabilities_reg,
+ capabilities_state,
+ condition,
+ expected_capabilities,
+):
+ """Test getting capabilities."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
- expected_capabilities = {
- "extra_fields": [
- {"name": "for", "optional": True, "type": "positive_time_period_dict"}
- ]
- }
- conditions = await async_get_device_automations(hass, "condition", device_entry.id)
- for condition in conditions:
- capabilities = await async_get_device_automation_capabilities(
- hass, "condition", condition
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ "5678",
+ device_id=device_entry.id,
+ capabilities=capabilities_reg,
+ )
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678",
+ STATE_ON,
+ capabilities_state,
)
- assert capabilities == expected_capabilities
+
+ capabilities = await device_condition.async_get_condition_capabilities(
+ hass,
+ {
+ "domain": DOMAIN,
+ "device_id": "abcdefgh",
+ "entity_id": f"{DOMAIN}.test_5678",
+ "type": condition,
+ },
+ )
+
+ assert capabilities and "extra_fields" in capabilities
+
+ assert (
+ voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ )
+ == expected_capabilities
+ )
+
+
+@pytest.mark.parametrize(
+ "condition,capability_name,extra",
+ [
+ ("is_mode", "mode", {"type": "select", "options": []}),
+ ],
+)
+async def test_capabilities_missing_entity(
+ hass, device_reg, entity_reg, condition, capability_name, extra
+):
+ """Test getting capabilities."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+
+ capabilities = await device_condition.async_get_condition_capabilities(
+ hass,
+ {
+ "domain": DOMAIN,
+ "device_id": "abcdefgh",
+ "entity_id": f"{DOMAIN}.test_5678",
+ "type": condition,
+ },
+ )
+
+ expected_capabilities = [
+ {
+ "name": capability_name,
+ "required": True,
+ **extra,
+ }
+ ]
+
+ assert capabilities and "extra_fields" in capabilities
+
+ assert (
+ voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ )
+ == expected_capabilities
+ )
diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py
index ac77a5a0407..0789a344ec6 100644
--- a/tests/components/hyperion/__init__.py
+++ b/tests/components/hyperion/__init__.py
@@ -125,7 +125,7 @@ def add_test_config_entry(
options: dict[str, Any] | None = None,
) -> ConfigEntry:
"""Add a test config entry."""
- config_entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call]
+ config_entry: MockConfigEntry = MockConfigEntry(
entry_id=TEST_CONFIG_ENTRY_ID,
domain=DOMAIN,
data=data
@@ -137,7 +137,7 @@ def add_test_config_entry(
unique_id=TEST_SYSINFO_ID,
options=options or TEST_CONFIG_ENTRY_OPTIONS,
)
- config_entry.add_to_hass(hass) # type: ignore[no-untyped-call]
+ config_entry.add_to_hass(hass)
return config_entry
@@ -187,3 +187,12 @@ def register_test_entity(
suggested_object_id=entity_id,
disabled_by=None,
)
+
+
+async def async_call_registered_callback(
+ client: AsyncMock, key: str, *args: Any, **kwargs: Any
+) -> None:
+ """Call Hyperion entity callbacks that were registered with the client."""
+ for call in client.add_callbacks.call_args_list:
+ if key in call[0][0]:
+ await call[0][0][key](*args, **kwargs)
diff --git a/tests/components/hyperion/test_camera.py b/tests/components/hyperion/test_camera.py
new file mode 100644
index 00000000000..daed1ec2cc9
--- /dev/null
+++ b/tests/components/hyperion/test_camera.py
@@ -0,0 +1,211 @@
+"""Tests for the Hyperion integration."""
+from __future__ import annotations
+
+import asyncio
+import base64
+from collections.abc import Awaitable
+import logging
+from typing import Callable
+from unittest.mock import AsyncMock, Mock, patch
+
+from aiohttp import web
+import pytest
+
+from homeassistant.components.camera import (
+ DEFAULT_CONTENT_TYPE,
+ DOMAIN as CAMERA_DOMAIN,
+ async_get_image,
+ async_get_mjpeg_stream,
+)
+from homeassistant.components.hyperion import get_hyperion_device_id
+from homeassistant.components.hyperion.const import (
+ DOMAIN,
+ HYPERION_MANUFACTURER_NAME,
+ HYPERION_MODEL_NAME,
+ TYPE_HYPERION_CAMERA,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+
+from . import (
+ TEST_CONFIG_ENTRY_ID,
+ TEST_INSTANCE,
+ TEST_INSTANCE_1,
+ TEST_SYSINFO_ID,
+ async_call_registered_callback,
+ create_mock_client,
+ register_test_entity,
+ setup_test_config_entry,
+)
+
+_LOGGER = logging.getLogger(__name__)
+TEST_CAMERA_ENTITY_ID = "camera.test_instance_1"
+TEST_IMAGE_DATA = "TEST DATA"
+TEST_IMAGE_UPDATE = {
+ "command": "ledcolors-imagestream-update",
+ "result": {
+ "image": "data:image/jpg;base64,"
+ + base64.b64encode(TEST_IMAGE_DATA.encode()).decode("ascii"),
+ },
+ "success": True,
+}
+
+
+async def test_camera_setup(hass: HomeAssistant) -> None:
+ """Test turning the light on."""
+ client = create_mock_client()
+
+ await setup_test_config_entry(hass, hyperion_client=client)
+
+ # Verify switch is on (as per TEST_COMPONENTS above).
+ entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID)
+ assert entity_state
+ assert entity_state.state == "idle"
+
+
+async def test_camera_image(hass: HomeAssistant) -> None:
+ """Test retrieving a single camera image."""
+ client = create_mock_client()
+ client.async_send_image_stream_start = AsyncMock(return_value=True)
+ client.async_send_image_stream_stop = AsyncMock(return_value=True)
+
+ await setup_test_config_entry(hass, hyperion_client=client)
+
+ get_image_coro = async_get_image(hass, TEST_CAMERA_ENTITY_ID)
+ image_stream_update_coro = async_call_registered_callback(
+ client, "ledcolors-imagestream-update", TEST_IMAGE_UPDATE
+ )
+ result = await asyncio.gather(get_image_coro, image_stream_update_coro)
+
+ assert client.async_send_image_stream_start.called
+ assert client.async_send_image_stream_stop.called
+ assert result[0].content == TEST_IMAGE_DATA.encode()
+
+
+async def test_camera_invalid_image(hass: HomeAssistant) -> None:
+ """Test retrieving a single invalid camera image."""
+ client = create_mock_client()
+ client.async_send_image_stream_start = AsyncMock(return_value=True)
+ client.async_send_image_stream_stop = AsyncMock(return_value=True)
+
+ await setup_test_config_entry(hass, hyperion_client=client)
+
+ get_image_coro = async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=0)
+ image_stream_update_coro = async_call_registered_callback(
+ client, "ledcolors-imagestream-update", None
+ )
+ with pytest.raises(HomeAssistantError):
+ await asyncio.gather(get_image_coro, image_stream_update_coro)
+
+ get_image_coro = async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=0)
+ image_stream_update_coro = async_call_registered_callback(
+ client, "ledcolors-imagestream-update", {"garbage": 1}
+ )
+ with pytest.raises(HomeAssistantError):
+ await asyncio.gather(get_image_coro, image_stream_update_coro)
+
+ get_image_coro = async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=0)
+ image_stream_update_coro = async_call_registered_callback(
+ client,
+ "ledcolors-imagestream-update",
+ {"result": {"image": "data:image/jpg;base64,FOO"}},
+ )
+ with pytest.raises(HomeAssistantError):
+ await asyncio.gather(get_image_coro, image_stream_update_coro)
+
+
+async def test_camera_image_failed_start_stream_call(hass: HomeAssistant) -> None:
+ """Test retrieving a single camera image with failed start stream call."""
+ client = create_mock_client()
+ client.async_send_image_stream_start = AsyncMock(return_value=False)
+
+ await setup_test_config_entry(hass, hyperion_client=client)
+
+ with pytest.raises(HomeAssistantError):
+ await async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=0)
+
+ assert client.async_send_image_stream_start.called
+ assert not client.async_send_image_stream_stop.called
+
+
+async def test_camera_stream(hass: HomeAssistant) -> None:
+ """Test retrieving a camera stream."""
+ client = create_mock_client()
+ client.async_send_image_stream_start = AsyncMock(return_value=True)
+ client.async_send_image_stream_stop = AsyncMock(return_value=True)
+
+ request = Mock()
+
+ async def fake_get_still_stream(
+ in_request: web.Request,
+ callback: Callable[[], Awaitable[bytes | None]],
+ content_type: str,
+ interval: float,
+ ) -> bytes | None:
+ assert request == in_request
+ assert content_type == DEFAULT_CONTENT_TYPE
+ assert interval == 0.0
+ return await callback()
+
+ await setup_test_config_entry(hass, hyperion_client=client)
+
+ with patch(
+ "homeassistant.components.hyperion.camera.async_get_still_stream",
+ ) as fake:
+ fake.side_effect = fake_get_still_stream
+
+ get_stream_coro = async_get_mjpeg_stream(hass, request, TEST_CAMERA_ENTITY_ID)
+ image_stream_update_coro = async_call_registered_callback(
+ client, "ledcolors-imagestream-update", TEST_IMAGE_UPDATE
+ )
+ result = await asyncio.gather(get_stream_coro, image_stream_update_coro)
+
+ assert client.async_send_image_stream_start.called
+ assert client.async_send_image_stream_stop.called
+ assert result[0] == TEST_IMAGE_DATA.encode()
+
+
+async def test_camera_stream_failed_start_stream_call(hass: HomeAssistant) -> None:
+ """Test retrieving a camera stream with failed start stream call."""
+ client = create_mock_client()
+ client.async_send_image_stream_start = AsyncMock(return_value=False)
+
+ await setup_test_config_entry(hass, hyperion_client=client)
+
+ request = Mock()
+ assert not await async_get_mjpeg_stream(hass, request, TEST_CAMERA_ENTITY_ID)
+
+ assert client.async_send_image_stream_start.called
+ assert not client.async_send_image_stream_stop.called
+
+
+async def test_device_info(hass: HomeAssistant) -> None:
+ """Verify device information includes expected details."""
+ client = create_mock_client()
+
+ register_test_entity(
+ hass,
+ CAMERA_DOMAIN,
+ TYPE_HYPERION_CAMERA,
+ TEST_CAMERA_ENTITY_ID,
+ )
+ await setup_test_config_entry(hass, hyperion_client=client)
+
+ device_id = get_hyperion_device_id(TEST_SYSINFO_ID, TEST_INSTANCE)
+ device_registry = dr.async_get(hass)
+
+ device = device_registry.async_get_device({(DOMAIN, device_id)})
+ assert device
+ assert device.config_entries == {TEST_CONFIG_ENTRY_ID}
+ assert device.identifiers == {(DOMAIN, device_id)}
+ assert device.manufacturer == HYPERION_MANUFACTURER_NAME
+ assert device.model == HYPERION_MODEL_NAME
+ assert device.name == TEST_INSTANCE_1["friendly_name"]
+
+ entity_registry = await er.async_get_registry(hass)
+ entities_from_device = [
+ entry.entity_id
+ for entry in er.async_entries_for_device(entity_registry, device.id)
+ ]
+ assert TEST_CAMERA_ENTITY_ID in entities_from_device
diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py
index d8b12e3c72b..7260d589e71 100644
--- a/tests/components/hyperion/test_config_flow.py
+++ b/tests/components/hyperion/test_config_flow.py
@@ -2,7 +2,8 @@
from __future__ import annotations
import asyncio
-from typing import Any, Awaitable
+from collections.abc import Awaitable
+from typing import Any
from unittest.mock import AsyncMock, Mock, patch
from hyperion import const
@@ -26,6 +27,7 @@ from homeassistant.const import (
SERVICE_TURN_ON,
)
from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import FlowResult
from . import (
TEST_AUTH_REQUIRED_RESP,
@@ -100,7 +102,7 @@ TEST_SSDP_SERVICE_INFO = {
async def _create_mock_entry(hass: HomeAssistant) -> MockConfigEntry:
"""Add a test Hyperion entity to hass."""
- entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call]
+ entry: MockConfigEntry = MockConfigEntry(
entry_id=TEST_CONFIG_ENTRY_ID,
domain=DOMAIN,
unique_id=TEST_SYSINFO_ID,
@@ -111,7 +113,7 @@ async def _create_mock_entry(hass: HomeAssistant) -> MockConfigEntry:
"instance": TEST_INSTANCE,
},
)
- entry.add_to_hass(hass) # type: ignore[no-untyped-call]
+ entry.add_to_hass(hass)
# Setup
client = create_mock_client()
@@ -138,7 +140,7 @@ async def _init_flow(
async def _configure_flow(
- hass: HomeAssistant, result: dict, user_input: dict[str, Any] | None = None
+ hass: HomeAssistant, result: FlowResult, user_input: dict[str, Any] | None = None
) -> Any:
"""Provide input to a flow."""
user_input = user_input or {}
@@ -419,6 +421,11 @@ async def test_auth_create_token_approval_declined_task_canceled(
class CanceledAwaitableMock(AsyncMock):
"""A canceled awaitable mock."""
+ def __init__(self):
+ super().__init__()
+ self.done = Mock(return_value=False)
+ self.cancel = Mock()
+
def __await__(self) -> None:
raise asyncio.CancelledError
@@ -435,20 +442,15 @@ async def test_auth_create_token_approval_declined_task_canceled(
), patch(
"homeassistant.components.hyperion.config_flow.client.generate_random_auth_id",
return_value=TEST_AUTH_ID,
- ), patch.object(
- hass, "async_create_task", side_effect=create_task
):
result = await _configure_flow(
hass, result, user_input={CONF_CREATE_TOKEN: True}
)
assert result["step_id"] == "create_token"
- result = await _configure_flow(hass, result)
- assert result["step_id"] == "create_token_external"
-
- # Leave the task running, to ensure it is canceled.
- mock_task.done = Mock(return_value=False)
- mock_task.cancel = Mock()
+ with patch.object(hass, "async_create_task", side_effect=create_task):
+ result = await _configure_flow(hass, result)
+ assert result["step_id"] == "create_token_external"
result = await _configure_flow(hass, result)
diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py
index 0c6b2cf41df..829a76f22d3 100644
--- a/tests/components/hyperion/test_light.py
+++ b/tests/components/hyperion/test_light.py
@@ -241,7 +241,7 @@ async def test_setup_config_entry_dynamic_instances(hass: HomeAssistant) -> None
assert hass.states.get(TEST_ENTITY_ID_3) is not None
-async def test_light_basic_properies(hass: HomeAssistant) -> None:
+async def test_light_basic_properties(hass: HomeAssistant) -> None:
"""Test the basic properties."""
client = create_mock_client()
await setup_test_config_entry(hass, hyperion_client=client)
@@ -862,7 +862,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None:
assert client.async_client_disconnect.call_count == 2
-async def test_version_log_warning(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def]
+async def test_version_log_warning(caplog, hass: HomeAssistant) -> None:
"""Test warning on old version."""
client = create_mock_client()
client.async_sysinfo_version = AsyncMock(return_value="2.0.0-alpha.7")
@@ -871,7 +871,7 @@ async def test_version_log_warning(caplog, hass: HomeAssistant) -> None: # type
assert "Please consider upgrading" in caplog.text
-async def test_version_no_log_warning(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def]
+async def test_version_no_log_warning(caplog, hass: HomeAssistant) -> None:
"""Test no warning on acceptable version."""
client = create_mock_client()
client.async_sysinfo_version = AsyncMock(return_value="2.0.0-alpha.9")
@@ -1359,7 +1359,7 @@ async def test_lights_can_be_enabled(hass: HomeAssistant) -> None:
assert not updated_entry.disabled
await hass.async_block_till_done()
- async_fire_time_changed( # type: ignore[no-untyped-call]
+ async_fire_time_changed(
hass,
dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
@@ -1369,7 +1369,7 @@ async def test_lights_can_be_enabled(hass: HomeAssistant) -> None:
assert entity_state
-async def test_deprecated_effect_names(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def]
+async def test_deprecated_effect_names(caplog, hass: HomeAssistant) -> None:
"""Test deprecated effects function and issue a warning."""
client = create_mock_client()
client.async_send_clear = AsyncMock(return_value=True)
diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py
index 2367ad96133..e88db516eff 100644
--- a/tests/components/hyperion/test_switch.py
+++ b/tests/components/hyperion/test_switch.py
@@ -213,7 +213,7 @@ async def test_switches_can_be_enabled(hass: HomeAssistant) -> None:
assert not updated_entry.disabled
await hass.async_block_till_done()
- async_fire_time_changed( # type: ignore[no-untyped-call]
+ async_fire_time_changed(
hass,
dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py
index 9a353f59e42..1df106473f9 100644
--- a/tests/components/influxdb/test_sensor.py
+++ b/tests/components/influxdb/test_sensor.py
@@ -313,7 +313,7 @@ async def test_config_failure(hass, config_ext):
async def test_state_matches_query_result(
hass, mock_client, config_ext, queries, set_query_mock, make_resultset
):
- """Test state of sensor matches respone from query api."""
+ """Test state of sensor matches response from query api."""
set_query_mock(mock_client, return_value=make_resultset(42))
sensors = await _setup(hass, config_ext, queries, ["sensor.test"])
@@ -344,7 +344,7 @@ async def test_state_matches_query_result(
async def test_state_matches_first_query_result_for_multiple_return(
hass, caplog, mock_client, config_ext, queries, set_query_mock, make_resultset
):
- """Test state of sensor matches respone from query api."""
+ """Test state of sensor matches response from query api."""
set_query_mock(mock_client, return_value=make_resultset(42, "not used"))
sensors = await _setup(hass, config_ext, queries, ["sensor.test"])
@@ -370,7 +370,7 @@ async def test_state_matches_first_query_result_for_multiple_return(
async def test_state_for_no_results(
hass, caplog, mock_client, config_ext, queries, set_query_mock
):
- """Test state of sensor matches respone from query api."""
+ """Test state of sensor matches response from query api."""
set_query_mock(mock_client)
sensors = await _setup(hass, config_ext, queries, ["sensor.test"])
diff --git a/tests/components/kraken/conftest.py b/tests/components/kraken/conftest.py
new file mode 100644
index 00000000000..f34dedc4df9
--- /dev/null
+++ b/tests/components/kraken/conftest.py
@@ -0,0 +1,11 @@
+"""Provide common pytest fixtures for kraken tests."""
+from unittest.mock import patch
+
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def mock_call_rate_limit_sleep():
+ """Patch the call rate limit sleep time."""
+ with patch("homeassistant.components.kraken.CALL_RATE_LIMIT_SLEEP", new=0):
+ yield
diff --git a/tests/components/kraken/test_config_flow.py b/tests/components/kraken/test_config_flow.py
index 1a09fbe92c6..6015098e573 100644
--- a/tests/components/kraken/test_config_flow.py
+++ b/tests/components/kraken/test_config_flow.py
@@ -12,74 +12,61 @@ from tests.common import MockConfigEntry
async def test_config_flow(hass):
"""Test we can finish a config flow."""
with patch(
- "pykrakenapi.KrakenAPI.get_tradable_asset_pairs",
- return_value=TRADEABLE_ASSET_PAIR_RESPONSE,
- ), patch(
- "pykrakenapi.KrakenAPI.get_ticker_information",
- return_value=TICKER_INFORMATION_RESPONSE,
- ):
+ "homeassistant.components.kraken.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
- assert result["type"] == "create_entry"
await hass.async_block_till_done()
- state = hass.states.get("sensor.xbt_usd_ask")
- assert state
+
+ assert result["type"] == "create_entry"
+ assert len(mock_setup_entry.mock_calls) == 1
async def test_already_configured(hass):
"""Test we can not add a second config flow."""
- with patch(
- "pykrakenapi.KrakenAPI.get_tradable_asset_pairs",
- return_value=TRADEABLE_ASSET_PAIR_RESPONSE,
- ), patch(
- "pykrakenapi.KrakenAPI.get_ticker_information",
- return_value=TICKER_INFORMATION_RESPONSE,
- ):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": "user"}
- )
- assert result["type"] == "form"
+ MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
- result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
- assert result["type"] == "create_entry"
-
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": "user"}
- )
- assert result["type"] == "abort"
- assert result["reason"] == "already_configured"
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "user"}
+ )
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
async def test_options(hass):
"""Test options for Kraken."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ options={
+ CONF_SCAN_INTERVAL: 60,
+ CONF_TRACKED_ASSET_PAIRS: [
+ "ADA/XBT",
+ "ADA/ETH",
+ "XBT/EUR",
+ "XBT/GBP",
+ "XBT/USD",
+ "XBT/JPY",
+ ],
+ },
+ )
+ entry.add_to_hass(hass)
+
with patch(
+ "homeassistant.components.kraken.config_flow.KrakenAPI.get_tradable_asset_pairs",
+ return_value=TRADEABLE_ASSET_PAIR_RESPONSE,
+ ), patch(
"pykrakenapi.KrakenAPI.get_tradable_asset_pairs",
return_value=TRADEABLE_ASSET_PAIR_RESPONSE,
), patch(
"pykrakenapi.KrakenAPI.get_ticker_information",
return_value=TICKER_INFORMATION_RESPONSE,
):
- entry = MockConfigEntry(
- domain=DOMAIN,
- options={
- CONF_SCAN_INTERVAL: 60,
- CONF_TRACKED_ASSET_PAIRS: [
- "ADA/XBT",
- "ADA/ETH",
- "XBT/EUR",
- "XBT/GBP",
- "XBT/USD",
- "XBT/JPY",
- ],
- },
- )
- entry.add_to_hass(hass)
-
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
diff --git a/tests/components/kraken/test_init.py b/tests/components/kraken/test_init.py
index 742e48eb1c0..99af317154d 100644
--- a/tests/components/kraken/test_init.py
+++ b/tests/components/kraken/test_init.py
@@ -28,7 +28,7 @@ async def test_unload_entry(hass):
assert DOMAIN not in hass.data
-async def test_unkown_error(hass, caplog):
+async def test_unknown_error(hass, caplog):
"""Test unload for Kraken."""
with patch(
"pykrakenapi.KrakenAPI.get_tradable_asset_pairs",
diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py
index ea5eeb5a690..f51c79168d7 100644
--- a/tests/components/kulersky/test_light.py
+++ b/tests/components/kulersky/test_light.py
@@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch
import pykulersky
import pytest
+from pytest import approx
from homeassistant import setup
from homeassistant.components.kulersky.const import (
@@ -17,14 +18,9 @@ from homeassistant.components.light import (
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
- ATTR_WHITE_VALUE,
ATTR_XY_COLOR,
- COLOR_MODE_HS,
COLOR_MODE_RGBW,
SCAN_INTERVAL,
- SUPPORT_BRIGHTNESS,
- SUPPORT_COLOR,
- SUPPORT_WHITE_VALUE,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -72,12 +68,10 @@ async def test_init(hass, mock_light):
"""Test platform setup."""
state = hass.states.get("light.bedroom")
assert state.state == STATE_OFF
- assert state.attributes == {
+ assert dict(state.attributes) == {
ATTR_FRIENDLY_NAME: "Bedroom",
- ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW],
- ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
- | SUPPORT_COLOR
- | SUPPORT_WHITE_VALUE,
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
+ ATTR_SUPPORTED_FEATURES: 0,
}
with patch.object(hass.loop, "stop"):
@@ -129,7 +123,7 @@ async def test_light_turn_on(hass, mock_light):
await hass.async_block_till_done()
mock_light.set_color.assert_called_with(255, 255, 255, 255)
- mock_light.get_color.return_value = (50, 50, 50, 255)
+ mock_light.get_color.return_value = (50, 50, 50, 50)
await hass.services.async_call(
"light",
"turn_on",
@@ -137,9 +131,33 @@ async def test_light_turn_on(hass, mock_light):
blocking=True,
)
await hass.async_block_till_done()
- mock_light.set_color.assert_called_with(50, 50, 50, 255)
+ mock_light.set_color.assert_called_with(50, 50, 50, 50)
- mock_light.get_color.return_value = (50, 45, 25, 255)
+ mock_light.get_color.return_value = (50, 25, 13, 6)
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {ATTR_ENTITY_ID: "light.bedroom", ATTR_RGBW_COLOR: (255, 128, 64, 32)},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_light.set_color.assert_called_with(50, 25, 13, 6)
+
+ # RGB color is converted to RGBW by assigning the white component to the white
+ # channel, see color_rgb_to_rgbw
+ mock_light.get_color.return_value = (0, 17, 50, 17)
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {ATTR_ENTITY_ID: "light.bedroom", ATTR_RGB_COLOR: (64, 128, 255)},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_light.set_color.assert_called_with(0, 17, 50, 17)
+
+ # HS color is converted to RGBW by assigning the white component to the white
+ # channel, see color_rgb_to_rgbw
+ mock_light.get_color.return_value = (50, 41, 0, 50)
await hass.services.async_call(
"light",
"turn_on",
@@ -147,18 +165,7 @@ async def test_light_turn_on(hass, mock_light):
blocking=True,
)
await hass.async_block_till_done()
-
- mock_light.set_color.assert_called_with(50, 45, 25, 255)
-
- mock_light.get_color.return_value = (220, 201, 110, 180)
- await hass.services.async_call(
- "light",
- "turn_on",
- {ATTR_ENTITY_ID: "light.bedroom", ATTR_WHITE_VALUE: 180},
- blocking=True,
- )
- await hass.async_block_till_done()
- mock_light.set_color.assert_called_with(50, 45, 25, 180)
+ mock_light.set_color.assert_called_with(50, 41, 0, 50)
async def test_light_turn_off(hass, mock_light):
@@ -180,12 +187,10 @@ async def test_light_update(hass, mock_light):
state = hass.states.get("light.bedroom")
assert state.state == STATE_OFF
- assert state.attributes == {
+ assert dict(state.attributes) == {
ATTR_FRIENDLY_NAME: "Bedroom",
- ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW],
- ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
- | SUPPORT_COLOR
- | SUPPORT_WHITE_VALUE,
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
+ ATTR_SUPPORTED_FEATURES: 0,
}
# Test an exception during discovery
@@ -196,12 +201,50 @@ async def test_light_update(hass, mock_light):
state = hass.states.get("light.bedroom")
assert state.state == STATE_UNAVAILABLE
- assert state.attributes == {
+ assert dict(state.attributes) == {
ATTR_FRIENDLY_NAME: "Bedroom",
- ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW],
- ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
- | SUPPORT_COLOR
- | SUPPORT_WHITE_VALUE,
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
+ ATTR_SUPPORTED_FEATURES: 0,
+ }
+
+ mock_light.get_color.side_effect = None
+ mock_light.get_color.return_value = (80, 160, 255, 0)
+ utcnow = utcnow + SCAN_INTERVAL
+ async_fire_time_changed(hass, utcnow)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.bedroom")
+ assert state.state == STATE_ON
+ assert dict(state.attributes) == {
+ ATTR_FRIENDLY_NAME: "Bedroom",
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
+ ATTR_SUPPORTED_FEATURES: 0,
+ ATTR_COLOR_MODE: COLOR_MODE_RGBW,
+ ATTR_BRIGHTNESS: 255,
+ ATTR_HS_COLOR: (approx(212.571), approx(68.627)),
+ ATTR_RGB_COLOR: (80, 160, 255),
+ ATTR_RGBW_COLOR: (80, 160, 255, 0),
+ ATTR_XY_COLOR: (approx(0.17), approx(0.193)),
+ }
+
+ mock_light.get_color.side_effect = None
+ mock_light.get_color.return_value = (80, 160, 200, 255)
+ utcnow = utcnow + SCAN_INTERVAL
+ async_fire_time_changed(hass, utcnow)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.bedroom")
+ assert state.state == STATE_ON
+ assert dict(state.attributes) == {
+ ATTR_FRIENDLY_NAME: "Bedroom",
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
+ ATTR_SUPPORTED_FEATURES: 0,
+ ATTR_COLOR_MODE: COLOR_MODE_RGBW,
+ ATTR_BRIGHTNESS: 255,
+ ATTR_HS_COLOR: (approx(199.701), approx(26.275)),
+ ATTR_RGB_COLOR: (188, 233, 255),
+ ATTR_RGBW_COLOR: (80, 160, 200, 255),
+ ATTR_XY_COLOR: (approx(0.259), approx(0.306)),
}
mock_light.get_color.side_effect = None
@@ -212,17 +255,14 @@ async def test_light_update(hass, mock_light):
state = hass.states.get("light.bedroom")
assert state.state == STATE_ON
- assert state.attributes == {
+ assert dict(state.attributes) == {
ATTR_FRIENDLY_NAME: "Bedroom",
- ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW],
- ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
- | SUPPORT_COLOR
- | SUPPORT_WHITE_VALUE,
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
+ ATTR_SUPPORTED_FEATURES: 0,
ATTR_COLOR_MODE: COLOR_MODE_RGBW,
- ATTR_BRIGHTNESS: 200,
- ATTR_HS_COLOR: (200, 60),
- ATTR_RGB_COLOR: (102, 203, 255),
- ATTR_RGBW_COLOR: (102, 203, 255, 240),
- ATTR_WHITE_VALUE: 240,
- ATTR_XY_COLOR: (0.184, 0.261),
+ ATTR_BRIGHTNESS: 240,
+ ATTR_HS_COLOR: (approx(200.0), approx(27.059)),
+ ATTR_RGB_COLOR: (186, 232, 255),
+ ATTR_RGBW_COLOR: (85, 170, 212, 255),
+ ATTR_XY_COLOR: (approx(0.257), approx(0.305)),
}
diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py
new file mode 100644
index 00000000000..aae4acfa914
--- /dev/null
+++ b/tests/components/lcn/conftest.py
@@ -0,0 +1,97 @@
+"""Test configuration and mocks for LCN component."""
+import json
+from unittest.mock import AsyncMock, patch
+
+import pypck
+from pypck.connection import PchkConnectionManager
+import pypck.module
+from pypck.module import GroupConnection, ModuleConnection
+import pytest
+
+from homeassistant.components.lcn.const import DOMAIN
+from homeassistant.const import CONF_HOST
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry, load_fixture
+
+
+class MockModuleConnection(ModuleConnection):
+ """Fake a LCN module connection."""
+
+ status_request_handler = AsyncMock()
+ activate_status_request_handler = AsyncMock()
+ cancel_status_request_handler = AsyncMock()
+ send_command = AsyncMock(return_value=True)
+
+
+class MockGroupConnection(GroupConnection):
+ """Fake a LCN group connection."""
+
+ send_command = AsyncMock(return_value=True)
+
+
+class MockPchkConnectionManager(PchkConnectionManager):
+ """Fake connection handler."""
+
+ async def async_connect(self, timeout=30):
+ """Mock establishing a connection to PCHK."""
+ self.authentication_completed_future.set_result(True)
+ self.license_error_future.set_result(True)
+ self.segment_scan_completed_event.set()
+
+ async def async_close(self):
+ """Mock closing a connection to PCHK."""
+
+ @patch.object(pypck.connection, "ModuleConnection", MockModuleConnection)
+ @patch.object(pypck.connection, "GroupConnection", MockGroupConnection)
+ def get_address_conn(self, addr):
+ """Get LCN address connection."""
+ return super().get_address_conn(addr, request_serials=False)
+
+ send_command = AsyncMock()
+
+
+def create_config_entry(name):
+ """Set up config entries with configuration data."""
+ fixture_filename = f"lcn/config_entry_{name}.json"
+ entry_data = json.loads(load_fixture(fixture_filename))
+ options = {}
+
+ title = entry_data[CONF_HOST]
+ unique_id = fixture_filename
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title=title,
+ unique_id=unique_id,
+ data=entry_data,
+ options=options,
+ )
+ return entry
+
+
+@pytest.fixture(name="entry")
+def create_config_entry_pchk():
+ """Return one specific config entry."""
+ return create_config_entry("pchk")
+
+
+@pytest.fixture(name="entry2")
+def create_config_entry_myhome():
+ """Return one specific config entry."""
+ return create_config_entry("myhome")
+
+
+async def init_integration(hass, entry):
+ """Set up the LCN integration in Home Assistant."""
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+
+async def setup_component(hass):
+ """Set up the LCN component."""
+ fixture_filename = "lcn/config.json"
+ config_data = json.loads(load_fixture(fixture_filename))
+
+ await async_setup_component(hass, DOMAIN, config_data)
+ await hass.async_block_till_done()
diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py
new file mode 100644
index 00000000000..eef02b681d8
--- /dev/null
+++ b/tests/components/lcn/test_init.py
@@ -0,0 +1,106 @@
+"""Test init of LCN integration."""
+from unittest.mock import patch
+
+from pypck.connection import (
+ PchkAuthenticationError,
+ PchkConnectionManager,
+ PchkLicenseError,
+)
+
+from homeassistant import config_entries
+from homeassistant.components.lcn.const import DOMAIN
+from homeassistant.config_entries import ConfigEntryState
+from homeassistant.helpers import entity_registry as er
+from homeassistant.setup import async_setup_component
+
+from .conftest import MockPchkConnectionManager, init_integration, setup_component
+
+
+@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager)
+async def test_async_setup_entry(hass, entry):
+ """Test a successful setup entry and unload of entry."""
+ await init_integration(hass, entry)
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ assert entry.state == ConfigEntryState.LOADED
+
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert entry.state == ConfigEntryState.NOT_LOADED
+ assert not hass.data.get(DOMAIN)
+
+
+@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager)
+async def test_async_setup_multiple_entries(hass, entry, entry2):
+ """Test a successful setup and unload of multiple entries."""
+ for config_entry in (entry, entry2):
+ await init_integration(hass, config_entry)
+ assert config_entry.state == ConfigEntryState.LOADED
+ await hass.async_block_till_done()
+
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 2
+
+ for config_entry in (entry, entry2):
+ assert await hass.config_entries.async_unload(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert config_entry.state == ConfigEntryState.NOT_LOADED
+
+ assert not hass.data.get(DOMAIN)
+
+
+@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager)
+async def test_async_setup_entry_update(hass, entry):
+ """Test a successful setup entry if entry with same id already exists."""
+ # setup first entry
+ entry.source = config_entries.SOURCE_IMPORT
+
+ # create dummy entity for LCN platform as an orphan
+ entity_registry = await er.async_get_registry(hass)
+ dummy_entity = entity_registry.async_get_or_create(
+ "switch", DOMAIN, "dummy", config_entry=entry
+ )
+ assert dummy_entity in entity_registry.entities.values()
+
+ # add entity to hass and setup (should cleanup dummy entity)
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert dummy_entity not in entity_registry.entities.values()
+
+
+async def test_async_setup_entry_raises_authentication_error(hass, entry):
+ """Test that an authentication error is handled properly."""
+ with patch.object(
+ PchkConnectionManager, "async_connect", side_effect=PchkAuthenticationError
+ ):
+ await init_integration(hass, entry)
+ assert entry.state == ConfigEntryState.SETUP_ERROR
+
+
+async def test_async_setup_entry_raises_license_error(hass, entry):
+ """Test that an authentication error is handled properly."""
+ with patch.object(
+ PchkConnectionManager, "async_connect", side_effect=PchkLicenseError
+ ):
+ await init_integration(hass, entry)
+ assert entry.state == ConfigEntryState.SETUP_ERROR
+
+
+async def test_async_setup_entry_raises_timeout_error(hass, entry):
+ """Test that an authentication error is handled properly."""
+ with patch.object(PchkConnectionManager, "async_connect", side_effect=TimeoutError):
+ await init_integration(hass, entry)
+ assert entry.state == ConfigEntryState.SETUP_ERROR
+
+
+@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager)
+async def test_async_setup_from_configuration_yaml(hass):
+ """Test a successful setup using data from configuration.yaml."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ with patch("homeassistant.components.lcn.async_setup_entry") as async_setup_entry:
+ await setup_component(hass)
+
+ assert async_setup_entry.await_count == 2
diff --git a/tests/components/light/common.py b/tests/components/light/common.py
index 229823ceb17..0c16e0f2703 100644
--- a/tests/components/light/common.py
+++ b/tests/components/light/common.py
@@ -17,6 +17,7 @@ from homeassistant.components.light import (
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_TRANSITION,
+ ATTR_WHITE,
ATTR_WHITE_VALUE,
ATTR_XY_COLOR,
DOMAIN,
@@ -50,6 +51,7 @@ def turn_on(
flash=None,
effect=None,
color_name=None,
+ white=None,
):
"""Turn all or specified light on."""
hass.add_job(
@@ -71,6 +73,7 @@ def turn_on(
flash,
effect,
color_name,
+ white,
)
@@ -92,6 +95,7 @@ async def async_turn_on(
flash=None,
effect=None,
color_name=None,
+ white=None,
):
"""Turn all or specified light on."""
data = {
@@ -113,6 +117,7 @@ async def async_turn_on(
(ATTR_FLASH, flash),
(ATTR_EFFECT, effect),
(ATTR_COLOR_NAME, color_name),
+ (ATTR_WHITE, white),
]
if value is not None
}
diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py
index 842fb305c6c..d9394ae946e 100644
--- a/tests/components/light/test_init.py
+++ b/tests/components/light/test_init.py
@@ -1586,6 +1586,148 @@ async def test_light_service_call_color_conversion(hass, enable_custom_integrati
assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)}
+async def test_light_service_call_color_temp_emulation(
+ hass, enable_custom_integrations
+):
+ """Test color conversion in service calls."""
+ platform = getattr(hass.components, "test.light")
+ platform.init(empty=True)
+
+ platform.ENTITIES.append(platform.MockLight("Test_hs_ct", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_hs_white", STATE_ON))
+
+ entity0 = platform.ENTITIES[0]
+ entity0.supported_color_modes = {light.COLOR_MODE_COLOR_TEMP, light.COLOR_MODE_HS}
+
+ entity1 = platform.ENTITIES[1]
+ entity1.supported_color_modes = {light.COLOR_MODE_HS}
+
+ entity2 = platform.ENTITIES[2]
+ entity2.supported_color_modes = {light.COLOR_MODE_HS, light.COLOR_MODE_WHITE}
+
+ assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity0.entity_id)
+ assert state.attributes["supported_color_modes"] == [
+ light.COLOR_MODE_COLOR_TEMP,
+ light.COLOR_MODE_HS,
+ ]
+
+ state = hass.states.get(entity1.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_HS]
+
+ state = hass.states.get(entity2.entity_id)
+ assert state.attributes["supported_color_modes"] == [
+ light.COLOR_MODE_HS,
+ light.COLOR_MODE_WHITE,
+ ]
+
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {
+ "entity_id": [
+ entity0.entity_id,
+ entity1.entity_id,
+ entity2.entity_id,
+ ],
+ "brightness_pct": 100,
+ "color_temp": 200,
+ },
+ blocking=True,
+ )
+ _, data = entity0.last_call("turn_on")
+ assert data == {"brightness": 255, "color_temp": 200}
+ _, data = entity1.last_call("turn_on")
+ assert data == {"brightness": 255, "hs_color": (27.001, 19.243)}
+ _, data = entity2.last_call("turn_on")
+ assert data == {"brightness": 255, "hs_color": (27.001, 19.243)}
+
+
+async def test_light_service_call_white_mode(hass, enable_custom_integrations):
+ """Test color_mode white in service calls."""
+ platform = getattr(hass.components, "test.light")
+ platform.init(empty=True)
+
+ platform.ENTITIES.append(platform.MockLight("Test_white", STATE_ON))
+ entity0 = platform.ENTITIES[0]
+ entity0.supported_color_modes = {light.COLOR_MODE_HS, light.COLOR_MODE_WHITE}
+
+ assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity0.entity_id)
+ assert state.attributes["supported_color_modes"] == [
+ light.COLOR_MODE_HS,
+ light.COLOR_MODE_WHITE,
+ ]
+
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {
+ "entity_id": [entity0.entity_id],
+ "brightness_pct": 100,
+ "hs_color": (240, 100),
+ },
+ blocking=True,
+ )
+ _, data = entity0.last_call("turn_on")
+ assert data == {"brightness": 255, "hs_color": (240.0, 100.0)}
+
+ entity0.calls = []
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {"entity_id": [entity0.entity_id], "white": 50},
+ blocking=True,
+ )
+ _, data = entity0.last_call("turn_on")
+ assert data == {"white": 50}
+
+ entity0.calls = []
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {"entity_id": [entity0.entity_id], "white": 0},
+ blocking=True,
+ )
+ _, data = entity0.last_call("turn_off")
+ assert data == {}
+
+ entity0.calls = []
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {"entity_id": [entity0.entity_id], "brightness_pct": 100, "white": 50},
+ blocking=True,
+ )
+ _, data = entity0.last_call("turn_on")
+ assert data == {"white": 255}
+
+ entity0.calls = []
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {"entity_id": [entity0.entity_id], "brightness": 100, "white": 0},
+ blocking=True,
+ )
+ _, data = entity0.last_call("turn_on")
+ assert data == {"white": 100}
+
+ entity0.calls = []
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {"entity_id": [entity0.entity_id], "brightness_pct": 0, "white": 50},
+ blocking=True,
+ )
+ _, data = entity0.last_call("turn_off")
+ assert data == {}
+
+
async def test_light_state_color_conversion(hass, enable_custom_integrations):
"""Test color conversion in state updates."""
platform = getattr(hass.components, "test.light")
@@ -1801,3 +1943,42 @@ async def test_services_filter_parameters(
_, data = ent1.last_call("turn_off")
assert data == {}
+
+
+def test_valid_supported_color_modes():
+ """Test valid_supported_color_modes."""
+ supported = {light.COLOR_MODE_HS}
+ assert light.valid_supported_color_modes(supported) == supported
+
+ # Supported color modes must not be empty
+ supported = set()
+ with pytest.raises(vol.Error):
+ light.valid_supported_color_modes(supported)
+
+ # COLOR_MODE_WHITE must be combined with a color mode supporting color
+ supported = {light.COLOR_MODE_WHITE}
+ with pytest.raises(vol.Error):
+ light.valid_supported_color_modes(supported)
+
+ supported = {light.COLOR_MODE_WHITE, light.COLOR_MODE_COLOR_TEMP}
+ with pytest.raises(vol.Error):
+ light.valid_supported_color_modes(supported)
+
+ supported = {light.COLOR_MODE_WHITE, light.COLOR_MODE_HS}
+ assert light.valid_supported_color_modes(supported) == supported
+
+ # COLOR_MODE_ONOFF must be the only supported mode
+ supported = {light.COLOR_MODE_ONOFF}
+ assert light.valid_supported_color_modes(supported) == supported
+
+ supported = {light.COLOR_MODE_ONOFF, light.COLOR_MODE_COLOR_TEMP}
+ with pytest.raises(vol.Error):
+ light.valid_supported_color_modes(supported)
+
+ # COLOR_MODE_BRIGHTNESS must be the only supported mode
+ supported = {light.COLOR_MODE_BRIGHTNESS}
+ assert light.valid_supported_color_modes(supported) == supported
+
+ supported = {light.COLOR_MODE_BRIGHTNESS, light.COLOR_MODE_COLOR_TEMP}
+ with pytest.raises(vol.Error):
+ light.valid_supported_color_modes(supported)
diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py
index 815b8831d37..97d969acdd9 100644
--- a/tests/components/light/test_reproduce_state.py
+++ b/tests/components/light/test_reproduce_state.py
@@ -172,6 +172,7 @@ async def test_reproducing_states(hass, caplog):
light.COLOR_MODE_RGBW,
light.COLOR_MODE_RGBWW,
light.COLOR_MODE_UNKNOWN,
+ light.COLOR_MODE_WHITE,
light.COLOR_MODE_XY,
),
)
@@ -188,6 +189,7 @@ async def test_filter_color_modes(hass, caplog, color_mode):
**VALID_RGBW_COLOR,
**VALID_RGBWW_COLOR,
**VALID_XY_COLOR,
+ **VALID_BRIGHTNESS,
}
turn_on_calls = async_mock_service(hass, "light", "turn_on")
@@ -197,15 +199,23 @@ async def test_filter_color_modes(hass, caplog, color_mode):
)
expected_map = {
- light.COLOR_MODE_COLOR_TEMP: VALID_COLOR_TEMP,
- light.COLOR_MODE_BRIGHTNESS: {},
- light.COLOR_MODE_HS: VALID_HS_COLOR,
- light.COLOR_MODE_ONOFF: {},
- light.COLOR_MODE_RGB: VALID_RGB_COLOR,
- light.COLOR_MODE_RGBW: VALID_RGBW_COLOR,
- light.COLOR_MODE_RGBWW: VALID_RGBWW_COLOR,
- light.COLOR_MODE_UNKNOWN: {**VALID_HS_COLOR, **VALID_WHITE_VALUE},
- light.COLOR_MODE_XY: VALID_XY_COLOR,
+ light.COLOR_MODE_COLOR_TEMP: {**VALID_BRIGHTNESS, **VALID_COLOR_TEMP},
+ light.COLOR_MODE_BRIGHTNESS: VALID_BRIGHTNESS,
+ light.COLOR_MODE_HS: {**VALID_BRIGHTNESS, **VALID_HS_COLOR},
+ light.COLOR_MODE_ONOFF: {**VALID_BRIGHTNESS},
+ light.COLOR_MODE_RGB: {**VALID_BRIGHTNESS, **VALID_RGB_COLOR},
+ light.COLOR_MODE_RGBW: {**VALID_BRIGHTNESS, **VALID_RGBW_COLOR},
+ light.COLOR_MODE_RGBWW: {**VALID_BRIGHTNESS, **VALID_RGBWW_COLOR},
+ light.COLOR_MODE_UNKNOWN: {
+ **VALID_BRIGHTNESS,
+ **VALID_HS_COLOR,
+ **VALID_WHITE_VALUE,
+ },
+ light.COLOR_MODE_WHITE: {
+ **VALID_BRIGHTNESS,
+ light.ATTR_WHITE: VALID_BRIGHTNESS[light.ATTR_BRIGHTNESS],
+ },
+ light.COLOR_MODE_XY: {**VALID_BRIGHTNESS, **VALID_XY_COLOR},
}
expected = expected_map[color_mode]
@@ -213,6 +223,13 @@ async def test_filter_color_modes(hass, caplog, color_mode):
assert turn_on_calls[0].domain == "light"
assert dict(turn_on_calls[0].data) == {"entity_id": "light.entity", **expected}
+ # 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})]
+ )
+ assert len(turn_on_calls) == 1
+
async def test_deprecation_warning(hass, caplog):
"""Test deprecation warning."""
diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py
index a84555bdd42..c5a9b19d949 100644
--- a/tests/components/lock/test_device_action.py
+++ b/tests/components/lock/test_device_action.py
@@ -2,8 +2,7 @@
import pytest
import homeassistant.components.automation as automation
-from homeassistant.components.lock import DOMAIN
-from homeassistant.const import CONF_PLATFORM
+from homeassistant.components.lock import DOMAIN, SUPPORT_OPEN
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
@@ -30,15 +29,25 @@ def entity_reg(hass):
return mock_registry(hass)
-async def test_get_actions_support_open(
- hass, device_reg, entity_reg, enable_custom_integrations
+@pytest.mark.parametrize(
+ "set_state,features_reg,features_state,expected_action_types",
+ [
+ (False, 0, 0, []),
+ (False, SUPPORT_OPEN, 0, ["open"]),
+ (True, 0, 0, []),
+ (True, 0, SUPPORT_OPEN, ["open"]),
+ ],
+)
+async def test_get_actions(
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ features_reg,
+ features_state,
+ expected_action_types,
):
- """Test we get the expected actions from a lock which supports open."""
- platform = getattr(hass.components, f"test.{DOMAIN}")
- platform.init()
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
- await hass.async_block_till_done()
-
+ """Test we get the expected actions from a lock."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
@@ -48,69 +57,33 @@ async def test_get_actions_support_open(
entity_reg.async_get_or_create(
DOMAIN,
"test",
- platform.ENTITIES["support_open"].unique_id,
+ "5678",
device_id=device_entry.id,
+ supported_features=features_reg,
)
-
- expected_actions = [
+ if set_state:
+ hass.states.async_set(
+ f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state}
+ )
+ expected_actions = []
+ basic_action_types = ["lock", "unlock"]
+ expected_actions += [
{
"domain": DOMAIN,
- "type": "lock",
+ "type": action,
"device_id": device_entry.id,
- "entity_id": "lock.support_open_lock",
- },
- {
- "domain": DOMAIN,
- "type": "unlock",
- "device_id": device_entry.id,
- "entity_id": "lock.support_open_lock",
- },
- {
- "domain": DOMAIN,
- "type": "open",
- "device_id": device_entry.id,
- "entity_id": "lock.support_open_lock",
- },
+ "entity_id": f"{DOMAIN}.test_5678",
+ }
+ for action in basic_action_types
]
- actions = await async_get_device_automations(hass, "action", device_entry.id)
- assert_lists_same(actions, expected_actions)
-
-
-async def test_get_actions_not_support_open(
- hass, device_reg, entity_reg, enable_custom_integrations
-):
- """Test we get the expected actions from a lock which doesn't support open."""
- platform = getattr(hass.components, f"test.{DOMAIN}")
- platform.init()
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
- await hass.async_block_till_done()
-
- config_entry = MockConfigEntry(domain="test", data={})
- config_entry.add_to_hass(hass)
- device_entry = device_reg.async_get_or_create(
- config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
- )
- entity_reg.async_get_or_create(
- DOMAIN,
- "test",
- platform.ENTITIES["no_support_open"].unique_id,
- device_id=device_entry.id,
- )
-
- expected_actions = [
+ expected_actions += [
{
"domain": DOMAIN,
- "type": "lock",
+ "type": action,
"device_id": device_entry.id,
- "entity_id": "lock.no_support_open_lock",
- },
- {
- "domain": DOMAIN,
- "type": "unlock",
- "device_id": device_entry.id,
- "entity_id": "lock.no_support_open_lock",
- },
+ "entity_id": f"{DOMAIN}.test_5678",
+ }
+ for action in expected_action_types
]
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert_lists_same(actions, expected_actions)
diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py
index 0280e8f34fa..0c47ae8f2e0 100644
--- a/tests/components/mazda/test_init.py
+++ b/tests/components/mazda/test_init.py
@@ -7,7 +7,7 @@ from pymazda import MazdaAuthenticationException, MazdaException
import pytest
import voluptuous as vol
-from homeassistant.components.mazda.const import DOMAIN, SERVICES
+from homeassistant.components.mazda.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_EMAIL,
@@ -186,7 +186,23 @@ async def test_device_no_nickname(hass):
assert reg_device.name == "2021 MAZDA3 2.5 S SE AWD"
-async def test_services(hass):
+@pytest.mark.parametrize(
+ "service, service_data, expected_args",
+ [
+ ("start_charging", {}, [12345]),
+ ("start_engine", {}, [12345]),
+ ("stop_charging", {}, [12345]),
+ ("stop_engine", {}, [12345]),
+ ("turn_off_hazard_lights", {}, [12345]),
+ ("turn_on_hazard_lights", {}, [12345]),
+ (
+ "send_poi",
+ {"latitude": 1.2345, "longitude": 2.3456, "poi_name": "Work"},
+ [12345, 1.2345, 2.3456, "Work"],
+ ),
+ ],
+)
+async def test_services(hass, service, service_data, expected_args):
"""Test service calls."""
client_mock = await init_integration(hass)
@@ -196,21 +212,13 @@ async def test_services(hass):
)
device_id = reg_device.id
- for service in SERVICES:
- service_data = {"device_id": device_id}
- if service == "send_poi":
- service_data["latitude"] = 1.2345
- service_data["longitude"] = 2.3456
- service_data["poi_name"] = "Work"
+ service_data["device_id"] = device_id
- await hass.services.async_call(DOMAIN, service, service_data, blocking=True)
- await hass.async_block_till_done()
+ await hass.services.async_call(DOMAIN, service, service_data, blocking=True)
+ await hass.async_block_till_done()
- api_method = getattr(client_mock, service)
- if service == "send_poi":
- api_method.assert_called_once_with(12345, 1.2345, 2.3456, "Work")
- else:
- api_method.assert_called_once_with(12345)
+ api_method = getattr(client_mock, service)
+ api_method.assert_called_once_with(*expected_args)
async def test_service_invalid_device_id(hass):
diff --git a/tests/components/melcloud/test_atw_zone_sensor.py b/tests/components/melcloud/test_atw_zone_sensor.py
new file mode 100644
index 00000000000..6e6487a3774
--- /dev/null
+++ b/tests/components/melcloud/test_atw_zone_sensor.py
@@ -0,0 +1,51 @@
+"""Test the MELCloud ATW zone sensor."""
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant.components.melcloud.sensor import ATW_ZONE_SENSORS, AtwZoneSensor
+
+
+@pytest.fixture
+def mock_device():
+ """Mock MELCloud device."""
+ with patch("homeassistant.components.melcloud.MelCloudDevice") as mock:
+ mock.name = "name"
+ mock.device.serial = 1234
+ mock.device.mac = "11:11:11:11:11:11"
+ yield mock
+
+
+@pytest.fixture
+def mock_zone_1():
+ """Mock zone 1."""
+ with patch("pymelcloud.atw_device.Zone") as mock:
+ mock.zone_index = 1
+ yield mock
+
+
+@pytest.fixture
+def mock_zone_2():
+ """Mock zone 2."""
+ with patch("pymelcloud.atw_device.Zone") as mock:
+ mock.zone_index = 2
+ yield mock
+
+
+def test_zone_unique_ids(mock_device, mock_zone_1, mock_zone_2):
+ """Test unique id generation correctness."""
+ sensor_1 = AtwZoneSensor(
+ mock_device,
+ mock_zone_1,
+ "room_temperature",
+ ATW_ZONE_SENSORS["room_temperature"],
+ )
+ assert sensor_1.unique_id == "1234-11:11:11:11:11:11-room_temperature"
+
+ sensor_2 = AtwZoneSensor(
+ mock_device,
+ mock_zone_2,
+ "room_temperature",
+ ATW_ZONE_SENSORS["flow_temperature"],
+ )
+ assert sensor_2.unique_id == "1234-11:11:11:11:11:11-room_temperature-zone-2"
diff --git a/tests/components/metoffice/test_config_flow.py b/tests/components/metoffice/test_config_flow.py
index 8f01f4b9643..55dd54d1c87 100644
--- a/tests/components/metoffice/test_config_flow.py
+++ b/tests/components/metoffice/test_config_flow.py
@@ -68,6 +68,10 @@ async def test_form_already_configured(hass, requests_mock):
"/public/data/val/wxfcs/all/json/354107?res=3hourly",
text="",
)
+ requests_mock.get(
+ "/public/data/val/wxfcs/all/json/354107?res=daily",
+ text="",
+ )
MockConfigEntry(
domain=DOMAIN,
diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py
index 43f460056f9..e603d0f93f6 100644
--- a/tests/components/metoffice/test_sensor.py
+++ b/tests/components/metoffice/test_sensor.py
@@ -29,12 +29,17 @@ async def test_one_sensor_site_running(hass, requests_mock, legacy_patchable_tim
mock_json = json.loads(load_fixture("metoffice.json"))
all_sites = json.dumps(mock_json["all_sites"])
wavertree_hourly = json.dumps(mock_json["wavertree_hourly"])
+ wavertree_daily = json.dumps(mock_json["wavertree_daily"])
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites)
requests_mock.get(
"/public/data/val/wxfcs/all/json/354107?res=3hourly",
text=wavertree_hourly,
)
+ requests_mock.get(
+ "/public/data/val/wxfcs/all/json/354107?res=daily",
+ text=wavertree_daily,
+ )
entry = MockConfigEntry(
domain=DOMAIN,
@@ -72,15 +77,23 @@ async def test_two_sensor_sites_running(hass, requests_mock, legacy_patchable_ti
mock_json = json.loads(load_fixture("metoffice.json"))
all_sites = json.dumps(mock_json["all_sites"])
wavertree_hourly = json.dumps(mock_json["wavertree_hourly"])
+ wavertree_daily = json.dumps(mock_json["wavertree_daily"])
kingslynn_hourly = json.dumps(mock_json["kingslynn_hourly"])
+ kingslynn_daily = json.dumps(mock_json["kingslynn_daily"])
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites)
requests_mock.get(
"/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly
)
+ requests_mock.get(
+ "/public/data/val/wxfcs/all/json/354107?res=daily", text=wavertree_daily
+ )
requests_mock.get(
"/public/data/val/wxfcs/all/json/322380?res=3hourly", text=kingslynn_hourly
)
+ requests_mock.get(
+ "/public/data/val/wxfcs/all/json/322380?res=daily", text=kingslynn_daily
+ )
entry = MockConfigEntry(
domain=DOMAIN,
diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py
index 18edbc4a972..6d4187c7023 100644
--- a/tests/components/metoffice/test_weather.py
+++ b/tests/components/metoffice/test_weather.py
@@ -9,6 +9,7 @@ from homeassistant.util import utcnow
from . import NewDateTime
from .const import (
+ DATETIME_FORMAT,
METOFFICE_CONFIG_KINGSLYNN,
METOFFICE_CONFIG_WAVERTREE,
WAVERTREE_SENSOR_RESULTS,
@@ -26,6 +27,7 @@ async def test_site_cannot_connect(hass, requests_mock, legacy_patchable_time):
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text="")
requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=3hourly", text="")
+ requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=daily", text="")
entry = MockConfigEntry(
domain=DOMAIN,
@@ -35,9 +37,10 @@ async def test_site_cannot_connect(hass, requests_mock, legacy_patchable_time):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
- assert hass.states.get("weather.met_office_wavertree") is None
+ assert hass.states.get("weather.met_office_wavertree_3hourly") is None
+ assert hass.states.get("weather.met_office_wavertree_daily") is None
for sensor_id in WAVERTREE_SENSOR_RESULTS:
- sensor_name, sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id]
+ sensor_name, _ = WAVERTREE_SENSOR_RESULTS[sensor_id]
sensor = hass.states.get(f"sensor.wavertree_{sensor_name}")
assert sensor is None
@@ -53,11 +56,15 @@ async def test_site_cannot_update(hass, requests_mock, legacy_patchable_time):
mock_json = json.loads(load_fixture("metoffice.json"))
all_sites = json.dumps(mock_json["all_sites"])
wavertree_hourly = json.dumps(mock_json["wavertree_hourly"])
+ wavertree_daily = json.dumps(mock_json["wavertree_daily"])
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites)
requests_mock.get(
"/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly
)
+ requests_mock.get(
+ "/public/data/val/wxfcs/all/json/354107?res=daily", text=wavertree_daily
+ )
entry = MockConfigEntry(
domain=DOMAIN,
@@ -67,16 +74,23 @@ async def test_site_cannot_update(hass, requests_mock, legacy_patchable_time):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
- entity = hass.states.get("weather.met_office_wavertree")
+ entity = hass.states.get("weather.met_office_wavertree_3_hourly")
+ assert entity
+
+ entity = hass.states.get("weather.met_office_wavertree_daily")
assert entity
requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=3hourly", text="")
+ requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=daily", text="")
future_time = utcnow() + timedelta(minutes=20)
async_fire_time_changed(hass, future_time)
await hass.async_block_till_done()
- entity = hass.states.get("weather.met_office_wavertree")
+ entity = hass.states.get("weather.met_office_wavertree_3_hourly")
+ assert entity.state == STATE_UNAVAILABLE
+
+ entity = hass.states.get("weather.met_office_wavertree_daily")
assert entity.state == STATE_UNAVAILABLE
@@ -91,12 +105,17 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti
mock_json = json.loads(load_fixture("metoffice.json"))
all_sites = json.dumps(mock_json["all_sites"])
wavertree_hourly = json.dumps(mock_json["wavertree_hourly"])
+ wavertree_daily = json.dumps(mock_json["wavertree_daily"])
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites)
requests_mock.get(
"/public/data/val/wxfcs/all/json/354107?res=3hourly",
text=wavertree_hourly,
)
+ requests_mock.get(
+ "/public/data/val/wxfcs/all/json/354107?res=daily",
+ text=wavertree_daily,
+ )
entry = MockConfigEntry(
domain=DOMAIN,
@@ -106,8 +125,8 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
- # Wavertree weather platform expected results
- entity = hass.states.get("weather.met_office_wavertree")
+ # Wavertree 3-hourly weather platform expected results
+ entity = hass.states.get("weather.met_office_wavertree_3_hourly")
assert entity
assert entity.state == "sunny"
@@ -117,6 +136,41 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti
assert entity.attributes.get("visibility") == "Good - 10-20"
assert entity.attributes.get("humidity") == 50
+ # Forecasts added - just pick out 1 entry to check
+ assert len(entity.attributes.get("forecast")) == 35
+
+ assert (
+ entity.attributes.get("forecast")[26]["datetime"].strftime(DATETIME_FORMAT)
+ == "2020-04-28 21:00:00+0000"
+ )
+ assert entity.attributes.get("forecast")[26]["condition"] == "cloudy"
+ assert entity.attributes.get("forecast")[26]["temperature"] == 10
+ assert entity.attributes.get("forecast")[26]["wind_speed"] == 4
+ assert entity.attributes.get("forecast")[26]["wind_bearing"] == "NNE"
+
+ # Wavertree daily weather platform expected results
+ entity = hass.states.get("weather.met_office_wavertree_daily")
+ assert entity
+
+ assert entity.state == "sunny"
+ assert entity.attributes.get("temperature") == 19
+ assert entity.attributes.get("wind_speed") == 9
+ assert entity.attributes.get("wind_bearing") == "SSE"
+ assert entity.attributes.get("visibility") == "Good - 10-20"
+ assert entity.attributes.get("humidity") == 50
+
+ # Also has Forecasts added - again, just pick out 1 entry to check
+ assert len(entity.attributes.get("forecast")) == 8
+
+ assert (
+ entity.attributes.get("forecast")[7]["datetime"].strftime(DATETIME_FORMAT)
+ == "2020-04-29 12:00:00+0000"
+ )
+ assert entity.attributes.get("forecast")[7]["condition"] == "rainy"
+ assert entity.attributes.get("forecast")[7]["temperature"] == 13
+ assert entity.attributes.get("forecast")[7]["wind_speed"] == 13
+ assert entity.attributes.get("forecast")[7]["wind_bearing"] == "SE"
+
@patch(
"datapoint.Forecast.datetime.datetime",
@@ -129,15 +183,23 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t
mock_json = json.loads(load_fixture("metoffice.json"))
all_sites = json.dumps(mock_json["all_sites"])
wavertree_hourly = json.dumps(mock_json["wavertree_hourly"])
+ wavertree_daily = json.dumps(mock_json["wavertree_daily"])
kingslynn_hourly = json.dumps(mock_json["kingslynn_hourly"])
+ kingslynn_daily = json.dumps(mock_json["kingslynn_daily"])
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites)
requests_mock.get(
"/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly
)
+ requests_mock.get(
+ "/public/data/val/wxfcs/all/json/354107?res=daily", text=wavertree_daily
+ )
requests_mock.get(
"/public/data/val/wxfcs/all/json/322380?res=3hourly", text=kingslynn_hourly
)
+ requests_mock.get(
+ "/public/data/val/wxfcs/all/json/322380?res=daily", text=kingslynn_daily
+ )
entry = MockConfigEntry(
domain=DOMAIN,
@@ -153,8 +215,8 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t
await hass.config_entries.async_setup(entry2.entry_id)
await hass.async_block_till_done()
- # Wavertree weather platform expected results
- entity = hass.states.get("weather.met_office_wavertree")
+ # Wavertree 3-hourly weather platform expected results
+ entity = hass.states.get("weather.met_office_wavertree_3_hourly")
assert entity
assert entity.state == "sunny"
@@ -164,8 +226,43 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t
assert entity.attributes.get("visibility") == "Good - 10-20"
assert entity.attributes.get("humidity") == 50
- # King's Lynn weather platform expected results
- entity = hass.states.get("weather.met_office_king_s_lynn")
+ # Forecasts added - just pick out 1 entry to check
+ assert len(entity.attributes.get("forecast")) == 35
+
+ assert (
+ entity.attributes.get("forecast")[18]["datetime"].strftime(DATETIME_FORMAT)
+ == "2020-04-27 21:00:00+0000"
+ )
+ assert entity.attributes.get("forecast")[18]["condition"] == "sunny"
+ assert entity.attributes.get("forecast")[18]["temperature"] == 9
+ assert entity.attributes.get("forecast")[18]["wind_speed"] == 4
+ assert entity.attributes.get("forecast")[18]["wind_bearing"] == "NW"
+
+ # Wavertree daily weather platform expected results
+ entity = hass.states.get("weather.met_office_wavertree_daily")
+ assert entity
+
+ assert entity.state == "sunny"
+ assert entity.attributes.get("temperature") == 19
+ assert entity.attributes.get("wind_speed") == 9
+ assert entity.attributes.get("wind_bearing") == "SSE"
+ assert entity.attributes.get("visibility") == "Good - 10-20"
+ assert entity.attributes.get("humidity") == 50
+
+ # Also has Forecasts added - again, just pick out 1 entry to check
+ assert len(entity.attributes.get("forecast")) == 8
+
+ assert (
+ entity.attributes.get("forecast")[7]["datetime"].strftime(DATETIME_FORMAT)
+ == "2020-04-29 12:00:00+0000"
+ )
+ assert entity.attributes.get("forecast")[7]["condition"] == "rainy"
+ assert entity.attributes.get("forecast")[7]["temperature"] == 13
+ assert entity.attributes.get("forecast")[7]["wind_speed"] == 13
+ assert entity.attributes.get("forecast")[7]["wind_bearing"] == "SE"
+
+ # King's Lynn 3-hourly weather platform expected results
+ entity = hass.states.get("weather.met_office_king_s_lynn_3_hourly")
assert entity
assert entity.state == "sunny"
@@ -174,3 +271,38 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t
assert entity.attributes.get("wind_bearing") == "E"
assert entity.attributes.get("visibility") == "Very Good - 20-40"
assert entity.attributes.get("humidity") == 60
+
+ # Also has Forecast added - just pick out 1 entry to check
+ assert len(entity.attributes.get("forecast")) == 35
+
+ assert (
+ entity.attributes.get("forecast")[18]["datetime"].strftime(DATETIME_FORMAT)
+ == "2020-04-27 21:00:00+0000"
+ )
+ assert entity.attributes.get("forecast")[18]["condition"] == "cloudy"
+ assert entity.attributes.get("forecast")[18]["temperature"] == 10
+ assert entity.attributes.get("forecast")[18]["wind_speed"] == 7
+ assert entity.attributes.get("forecast")[18]["wind_bearing"] == "SE"
+
+ # King's Lynn daily weather platform expected results
+ entity = hass.states.get("weather.met_office_king_s_lynn_daily")
+ assert entity
+
+ assert entity.state == "cloudy"
+ assert entity.attributes.get("temperature") == 9
+ assert entity.attributes.get("wind_speed") == 4
+ assert entity.attributes.get("wind_bearing") == "ESE"
+ assert entity.attributes.get("visibility") == "Very Good - 20-40"
+ assert entity.attributes.get("humidity") == 75
+
+ # All should have Forecast added - again, just picking out 1 entry to check
+ assert len(entity.attributes.get("forecast")) == 8
+
+ assert (
+ entity.attributes.get("forecast")[5]["datetime"].strftime(DATETIME_FORMAT)
+ == "2020-04-28 12:00:00+0000"
+ )
+ assert entity.attributes.get("forecast")[5]["condition"] == "cloudy"
+ assert entity.attributes.get("forecast")[5]["temperature"] == 11
+ assert entity.attributes.get("forecast")[5]["wind_speed"] == 7
+ assert entity.attributes.get("forecast")[5]["wind_bearing"] == "ESE"
diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py
index 859c7d20d04..2159b58293b 100644
--- a/tests/components/mikrotik/test_hub.py
+++ b/tests/components/mikrotik/test_hub.py
@@ -12,7 +12,7 @@ from tests.common import MockConfigEntry
async def setup_mikrotik_entry(hass, **kwargs):
- """Set up Mikrotik intergation successfully."""
+ """Set up Mikrotik integration successfully."""
support_wireless = kwargs.get("support_wireless", True)
dhcp_data = kwargs.get("dhcp_data", DHCP_DATA)
wireless_data = kwargs.get("wireless_data", WIRELESS_DATA)
diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py
index d3ae1286ef1..db960f448ff 100644
--- a/tests/components/modbus/conftest.py
+++ b/tests/components/modbus/conftest.py
@@ -18,7 +18,7 @@ from homeassistant.const import (
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
-from tests.common import async_fire_time_changed
+from tests.common import async_fire_time_changed, mock_restore_cache
TEST_MODBUS_NAME = "modbusTest"
_LOGGER = logging.getLogger(__name__)
@@ -40,7 +40,7 @@ def mock_pymodbus():
@pytest.fixture
-async def mock_modbus(hass, mock_pymodbus):
+async def mock_modbus(hass, do_config):
"""Load integration modbus using mocked pymodbus."""
config = {
DOMAIN: [
@@ -49,14 +49,26 @@ async def mock_modbus(hass, mock_pymodbus):
CONF_HOST: "modbusTestHost",
CONF_PORT: 5501,
CONF_NAME: TEST_MODBUS_NAME,
+ **do_config,
}
]
}
- assert await async_setup_component(hass, DOMAIN, config) is True
- await hass.async_block_till_done()
- yield mock_pymodbus
+ with mock.patch(
+ "homeassistant.components.modbus.modbus.ModbusTcpClient", autospec=True
+ ) as mock_pb:
+ assert await async_setup_component(hass, DOMAIN, config) is True
+ await hass.async_block_till_done()
+ yield mock_pb
+@pytest.fixture
+async def mock_test_state(hass, request):
+ """Mock restore cache."""
+ mock_restore_cache(hass, request.param)
+ return request.param
+
+
+# dataclass
class ReadResult:
"""Storage class for register read results."""
@@ -80,6 +92,7 @@ async def base_test(
config_modbus=None,
scan_interval=None,
expect_init_to_fail=False,
+ expect_setup_to_fail=False,
):
"""Run test on device for given config."""
@@ -95,12 +108,9 @@ async def base_test(
mock_sync = mock.MagicMock()
with mock.patch(
- "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_sync
- ), mock.patch(
- "homeassistant.components.modbus.modbus.ModbusSerialClient",
+ "homeassistant.components.modbus.modbus.ModbusTcpClient",
+ autospec=True,
return_value=mock_sync,
- ), mock.patch(
- "homeassistant.components.modbus.modbus.ModbusUdpClient", return_value=mock_sync
):
# Setup inputs for the sensor
@@ -131,7 +141,10 @@ async def base_test(
{array_name_discovery: [{**config_device}]}
)
config_device = None
- assert await async_setup_component(hass, DOMAIN, config_modbus)
+ assert (
+ await async_setup_component(hass, DOMAIN, config_modbus)
+ is not expect_setup_to_fail
+ )
await hass.async_block_till_done()
# setup platform old style
@@ -151,7 +164,7 @@ async def base_test(
assert await async_setup_component(hass, entity_domain, config_device)
await hass.async_block_till_done()
- assert DOMAIN in hass.config.components
+ assert (DOMAIN in hass.config.components) is not expect_setup_to_fail
if config_device is not None:
entity_id = f"{entity_domain}.{device_name}"
device = hass.states.get(entity_id)
@@ -184,6 +197,7 @@ async def base_config_test(
method_discovery=False,
config_modbus=None,
expect_init_to_fail=False,
+ expect_setup_to_fail=False,
):
"""Check config of device for given config."""
@@ -200,6 +214,7 @@ async def base_config_test(
check_config_only=True,
config_modbus=config_modbus,
expect_init_to_fail=expect_init_to_fail,
+ expect_setup_to_fail=expect_setup_to_fail,
)
diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py
index 5089d0271dd..e9c178ff025 100644
--- a/tests/components/modbus/test_binary_sensor.py
+++ b/tests/components/modbus/test_binary_sensor.py
@@ -6,7 +6,6 @@ from homeassistant.components.modbus.const import (
CALL_TYPE_COIL,
CALL_TYPE_DISCRETE,
CONF_INPUT_TYPE,
- CONF_INPUTS,
)
from homeassistant.const import (
CONF_ADDRESS,
@@ -20,40 +19,39 @@ from homeassistant.const import (
)
from homeassistant.core import State
-from .conftest import ReadResult, base_config_test, base_test, prepare_service_update
+from .conftest import ReadResult, base_test, prepare_service_update
-from tests.common import mock_restore_cache
+SENSOR_NAME = "test_binary_sensor"
+ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}"
-@pytest.mark.parametrize("do_discovery", [False, True])
@pytest.mark.parametrize(
- "do_options",
+ "do_config",
[
- {},
{
- CONF_SLAVE: 10,
- CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
- CONF_DEVICE_CLASS: "door",
+ CONF_BINARY_SENSORS: [
+ {
+ CONF_NAME: SENSOR_NAME,
+ CONF_ADDRESS: 51,
+ }
+ ]
+ },
+ {
+ CONF_BINARY_SENSORS: [
+ {
+ CONF_NAME: SENSOR_NAME,
+ CONF_ADDRESS: 51,
+ CONF_SLAVE: 10,
+ CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
+ CONF_DEVICE_CLASS: "door",
+ }
+ ]
},
],
)
-async def test_config_binary_sensor(hass, do_discovery, do_options):
- """Run test for binary sensor."""
- sensor_name = "test_sensor"
- config_sensor = {
- CONF_NAME: sensor_name,
- CONF_ADDRESS: 51,
- **do_options,
- }
- await base_config_test(
- hass,
- config_sensor,
- sensor_name,
- SENSOR_DOMAIN,
- CONF_BINARY_SENSORS,
- CONF_INPUTS,
- method_discovery=do_discovery,
- )
+async def test_config_binary_sensor(hass, mock_modbus):
+ """Run config test for binary sensor."""
+ assert SENSOR_DOMAIN in hass.config.components
@pytest.mark.parametrize("do_type", [CALL_TYPE_COIL, CALL_TYPE_DISCRETE])
@@ -88,14 +86,13 @@ async def test_config_binary_sensor(hass, do_discovery, do_options):
)
async def test_all_binary_sensor(hass, do_type, regs, expected):
"""Run test for given config."""
- sensor_name = "modbus_test_binary_sensor"
state = await base_test(
hass,
- {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: do_type},
- sensor_name,
+ {CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: do_type},
+ SENSOR_NAME,
SENSOR_DOMAIN,
CONF_BINARY_SENSORS,
- CONF_INPUTS,
+ None,
regs,
expected,
method_discovery=True,
@@ -107,11 +104,10 @@ async def test_all_binary_sensor(hass, do_type, regs, expected):
async def test_service_binary_sensor_update(hass, mock_pymodbus):
"""Run test for service homeassistant.update_entity."""
- entity_id = "binary_sensor.test"
config = {
CONF_BINARY_SENSORS: [
{
- CONF_NAME: "test",
+ CONF_NAME: SENSOR_NAME,
CONF_ADDRESS: 1234,
CONF_INPUT_TYPE: CALL_TYPE_COIL,
}
@@ -123,36 +119,36 @@ async def test_service_binary_sensor_update(hass, mock_pymodbus):
config,
)
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
mock_pymodbus.read_coils.return_value = ReadResult([0x01])
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == STATE_ON
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
-async def test_restore_state_binary_sensor(hass):
+@pytest.mark.parametrize(
+ "mock_test_state",
+ [(State(ENTITY_ID, STATE_ON),)],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "do_config",
+ [
+ {
+ CONF_BINARY_SENSORS: [
+ {
+ CONF_NAME: SENSOR_NAME,
+ CONF_ADDRESS: 51,
+ }
+ ]
+ },
+ ],
+)
+async def test_restore_state_binary_sensor(hass, mock_test_state, mock_modbus):
"""Run test for binary sensor restore state."""
-
- sensor_name = "test_binary_sensor"
- test_value = STATE_ON
- config_sensor = {CONF_NAME: sensor_name, CONF_ADDRESS: 17}
- mock_restore_cache(
- hass,
- (State(f"{SENSOR_DOMAIN}.{sensor_name}", test_value),),
- )
- await base_config_test(
- hass,
- config_sensor,
- sensor_name,
- SENSOR_DOMAIN,
- CONF_BINARY_SENSORS,
- None,
- method_discovery=True,
- )
- entity_id = f"{SENSOR_DOMAIN}.{sensor_name}"
- assert hass.states.get(entity_id).state == test_value
+ assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state
diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py
index c73a73e47e8..b58822644be 100644
--- a/tests/components/modbus/test_climate.py
+++ b/tests/components/modbus/test_climate.py
@@ -3,54 +3,53 @@ import pytest
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.climate.const import HVAC_MODE_AUTO
-from homeassistant.components.modbus.const import (
- CONF_CLIMATES,
- CONF_CURRENT_TEMP,
- CONF_DATA_COUNT,
- CONF_TARGET_TEMP,
-)
+from homeassistant.components.modbus.const import CONF_CLIMATES, CONF_TARGET_TEMP
from homeassistant.const import (
ATTR_TEMPERATURE,
+ CONF_ADDRESS,
+ CONF_COUNT,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_SLAVE,
)
from homeassistant.core import State
-from .conftest import ReadResult, base_config_test, base_test, prepare_service_update
+from .conftest import ReadResult, base_test, prepare_service_update
-from tests.common import mock_restore_cache
+CLIMATE_NAME = "test_climate"
+ENTITY_ID = f"{CLIMATE_DOMAIN}.{CLIMATE_NAME}"
@pytest.mark.parametrize(
- "do_options",
+ "do_config",
[
- {},
{
- CONF_SCAN_INTERVAL: 20,
- CONF_DATA_COUNT: 2,
+ CONF_CLIMATES: [
+ {
+ CONF_NAME: CLIMATE_NAME,
+ CONF_TARGET_TEMP: 117,
+ CONF_ADDRESS: 117,
+ CONF_SLAVE: 10,
+ }
+ ],
+ },
+ {
+ CONF_CLIMATES: [
+ {
+ CONF_NAME: CLIMATE_NAME,
+ CONF_TARGET_TEMP: 117,
+ CONF_ADDRESS: 117,
+ CONF_SLAVE: 10,
+ CONF_SCAN_INTERVAL: 20,
+ CONF_COUNT: 2,
+ }
+ ],
},
],
)
-async def test_config_climate(hass, do_options):
- """Run test for climate."""
- device_name = "test_climate"
- device_config = {
- CONF_NAME: device_name,
- CONF_TARGET_TEMP: 117,
- CONF_CURRENT_TEMP: 117,
- CONF_SLAVE: 10,
- **do_options,
- }
- await base_config_test(
- hass,
- device_config,
- device_name,
- CLIMATE_DOMAIN,
- CONF_CLIMATES,
- None,
- method_discovery=True,
- )
+async def test_config_climate(hass, mock_modbus):
+ """Run configuration test for climate."""
+ assert CLIMATE_DOMAIN in hass.config.components
@pytest.mark.parametrize(
@@ -64,18 +63,18 @@ async def test_config_climate(hass, do_options):
)
async def test_temperature_climate(hass, regs, expected):
"""Run test for given config."""
- climate_name = "modbus_test_climate"
+ CLIMATE_NAME = "modbus_test_climate"
return
state = await base_test(
hass,
{
- CONF_NAME: climate_name,
+ CONF_NAME: CLIMATE_NAME,
CONF_SLAVE: 1,
CONF_TARGET_TEMP: 117,
- CONF_CURRENT_TEMP: 117,
- CONF_DATA_COUNT: 2,
+ CONF_ADDRESS: 117,
+ CONF_COUNT: 2,
},
- climate_name,
+ CLIMATE_NAME,
CLIMATE_DOMAIN,
CONF_CLIMATES,
None,
@@ -90,13 +89,12 @@ async def test_temperature_climate(hass, regs, expected):
async def test_service_climate_update(hass, mock_pymodbus):
"""Run test for service homeassistant.update_entity."""
- entity_id = "climate.test"
config = {
CONF_CLIMATES: [
{
- CONF_NAME: "test",
+ CONF_NAME: CLIMATE_NAME,
CONF_TARGET_TEMP: 117,
- CONF_CURRENT_TEMP: 117,
+ CONF_ADDRESS: 117,
CONF_SLAVE: 10,
}
]
@@ -107,37 +105,36 @@ async def test_service_climate_update(hass, mock_pymodbus):
config,
)
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == "auto"
+ assert hass.states.get(ENTITY_ID).state == "auto"
-async def test_restore_state_climate(hass):
+test_value = State(ENTITY_ID, 35)
+test_value.attributes = {ATTR_TEMPERATURE: 37}
+
+
+@pytest.mark.parametrize(
+ "mock_test_state",
+ [(test_value,)],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "do_config",
+ [
+ {
+ CONF_CLIMATES: [
+ {
+ CONF_NAME: CLIMATE_NAME,
+ CONF_TARGET_TEMP: 117,
+ CONF_ADDRESS: 117,
+ }
+ ],
+ },
+ ],
+)
+async def test_restore_state_climate(hass, mock_test_state, mock_modbus):
"""Run test for sensor restore state."""
-
- climate_name = "test_climate"
- test_temp = 37
- entity_id = f"{CLIMATE_DOMAIN}.{climate_name}"
- test_value = State(entity_id, 35)
- test_value.attributes = {ATTR_TEMPERATURE: test_temp}
- config_sensor = {
- CONF_NAME: climate_name,
- CONF_TARGET_TEMP: 117,
- CONF_CURRENT_TEMP: 117,
- }
- mock_restore_cache(
- hass,
- (test_value,),
- )
- await base_config_test(
- hass,
- config_sensor,
- climate_name,
- CLIMATE_DOMAIN,
- CONF_CLIMATES,
- None,
- method_discovery=True,
- )
- state = hass.states.get(entity_id)
+ state = hass.states.get(ENTITY_ID)
assert state.state == HVAC_MODE_AUTO
- assert state.attributes[ATTR_TEMPERATURE] == test_temp
+ assert state.attributes[ATTR_TEMPERATURE] == 37
diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py
index 8fbb45fde8e..37274603bee 100644
--- a/tests/components/modbus/test_cover.py
+++ b/tests/components/modbus/test_cover.py
@@ -1,5 +1,4 @@
"""The tests for the Modbus cover component."""
-import logging
from pymodbus.exceptions import ModbusException
import pytest
@@ -8,7 +7,7 @@ from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
from homeassistant.components.modbus.const import (
CALL_TYPE_COIL,
CALL_TYPE_REGISTER_HOLDING,
- CONF_REGISTER,
+ CONF_INPUT_TYPE,
CONF_STATE_CLOSED,
CONF_STATE_CLOSING,
CONF_STATE_OPEN,
@@ -17,6 +16,7 @@ from homeassistant.components.modbus.const import (
CONF_STATUS_REGISTER_TYPE,
)
from homeassistant.const import (
+ CONF_ADDRESS,
CONF_COVERS,
CONF_NAME,
CONF_SCAN_INTERVAL,
@@ -29,39 +29,40 @@ from homeassistant.const import (
)
from homeassistant.core import State
-from .conftest import ReadResult, base_config_test, base_test, prepare_service_update
+from .conftest import ReadResult, base_test, prepare_service_update
-from tests.common import mock_restore_cache
+COVER_NAME = "test_cover"
+ENTITY_ID = f"{COVER_DOMAIN}.{COVER_NAME}"
@pytest.mark.parametrize(
- "do_options",
+ "do_config",
[
- {},
{
- CONF_SLAVE: 10,
- CONF_SCAN_INTERVAL: 20,
+ CONF_COVERS: [
+ {
+ CONF_NAME: COVER_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_INPUT_TYPE: CALL_TYPE_COIL,
+ }
+ ]
+ },
+ {
+ CONF_COVERS: [
+ {
+ CONF_NAME: COVER_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_SLAVE: 10,
+ CONF_SCAN_INTERVAL: 20,
+ }
+ ]
},
],
)
-@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CONF_REGISTER])
-async def test_config_cover(hass, do_options, read_type):
- """Run test for cover."""
- device_name = "test_cover"
- device_config = {
- CONF_NAME: device_name,
- read_type: 1234,
- **do_options,
- }
- await base_config_test(
- hass,
- device_config,
- device_name,
- COVER_DOMAIN,
- CONF_COVERS,
- None,
- method_discovery=True,
- )
+async def test_config_cover(hass, mock_modbus):
+ """Run configuration test for cover."""
+ assert COVER_DOMAIN in hass.config.components
@pytest.mark.parametrize(
@@ -91,15 +92,15 @@ async def test_config_cover(hass, do_options, read_type):
)
async def test_coil_cover(hass, regs, expected):
"""Run test for given config."""
- cover_name = "modbus_test_cover"
state = await base_test(
hass,
{
- CONF_NAME: cover_name,
- CALL_TYPE_COIL: 1234,
+ CONF_NAME: COVER_NAME,
+ CONF_INPUT_TYPE: CALL_TYPE_COIL,
+ CONF_ADDRESS: 1234,
CONF_SLAVE: 1,
},
- cover_name,
+ COVER_NAME,
COVER_DOMAIN,
CONF_COVERS,
None,
@@ -138,15 +139,14 @@ async def test_coil_cover(hass, regs, expected):
)
async def test_register_cover(hass, regs, expected):
"""Run test for given config."""
- cover_name = "modbus_test_cover"
state = await base_test(
hass,
{
- CONF_NAME: cover_name,
- CONF_REGISTER: 1234,
+ CONF_NAME: COVER_NAME,
+ CONF_ADDRESS: 1234,
CONF_SLAVE: 1,
},
- cover_name,
+ COVER_NAME,
COVER_DOMAIN,
CONF_COVERS,
None,
@@ -158,44 +158,14 @@ async def test_register_cover(hass, regs, expected):
assert state == expected
-@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CONF_REGISTER])
-async def test_unsupported_config_cover(hass, read_type, caplog):
- """
- Run test for cover.
-
- Initialize the Cover in the legacy manner via platform.
- This test expects that the Cover won't be initialized, and that we get a config warning.
- """
- device_name = "test_cover"
- device_config = {CONF_NAME: device_name, read_type: 1234}
-
- caplog.set_level(logging.WARNING)
- caplog.clear()
-
- await base_config_test(
- hass,
- device_config,
- device_name,
- COVER_DOMAIN,
- CONF_COVERS,
- None,
- method_discovery=False,
- expect_init_to_fail=True,
- )
-
- assert len(caplog.records) == 1
- assert caplog.records[0].levelname == "WARNING"
-
-
async def test_service_cover_update(hass, mock_pymodbus):
"""Run test for service homeassistant.update_entity."""
- entity_id = "cover.test"
config = {
CONF_COVERS: [
{
- CONF_NAME: "test",
- CONF_REGISTER: 1234,
+ CONF_NAME: COVER_NAME,
+ CONF_ADDRESS: 1234,
CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
}
]
@@ -206,65 +176,67 @@ async def test_service_cover_update(hass, mock_pymodbus):
config,
)
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == STATE_CLOSED
+ assert hass.states.get(ENTITY_ID).state == STATE_CLOSED
mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01])
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == STATE_OPEN
+ assert hass.states.get(ENTITY_ID).state == STATE_OPEN
@pytest.mark.parametrize(
- "state", [STATE_CLOSED, STATE_CLOSING, STATE_OPENING, STATE_OPEN]
+ "mock_test_state",
+ [
+ (State(ENTITY_ID, STATE_CLOSED),),
+ (State(ENTITY_ID, STATE_CLOSING),),
+ (State(ENTITY_ID, STATE_OPENING),),
+ (State(ENTITY_ID, STATE_OPEN),),
+ ],
+ indirect=True,
)
-async def test_restore_state_cover(hass, state):
+@pytest.mark.parametrize(
+ "do_config",
+ [
+ {
+ CONF_COVERS: [
+ {
+ CONF_NAME: COVER_NAME,
+ CONF_INPUT_TYPE: CALL_TYPE_COIL,
+ CONF_ADDRESS: 1234,
+ CONF_STATE_OPEN: 1,
+ CONF_STATE_CLOSED: 0,
+ CONF_STATE_OPENING: 2,
+ CONF_STATE_CLOSING: 3,
+ CONF_STATUS_REGISTER: 1234,
+ CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ }
+ ]
+ },
+ ],
+)
+async def test_restore_state_cover(hass, mock_test_state, mock_modbus):
"""Run test for cover restore state."""
-
- entity_id = "cover.test"
- cover_name = "test"
- config = {
- CONF_NAME: cover_name,
- CALL_TYPE_COIL: 1234,
- CONF_STATE_OPEN: 1,
- CONF_STATE_CLOSED: 0,
- CONF_STATE_OPENING: 2,
- CONF_STATE_CLOSING: 3,
- CONF_STATUS_REGISTER: 1234,
- CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
- }
- mock_restore_cache(
- hass,
- (State(f"{entity_id}", state),),
- )
- await base_config_test(
- hass,
- config,
- cover_name,
- COVER_DOMAIN,
- CONF_COVERS,
- None,
- method_discovery=True,
- )
- assert hass.states.get(entity_id).state == state
+ test_state = mock_test_state[0].state
+ assert hass.states.get(ENTITY_ID).state == test_state
async def test_service_cover_move(hass, mock_pymodbus):
"""Run test for service homeassistant.update_entity."""
- entity_id = "cover.test"
- entity_id2 = "cover.test2"
+ ENTITY_ID2 = f"{ENTITY_ID}2"
config = {
CONF_COVERS: [
{
- CONF_NAME: "test",
- CONF_REGISTER: 1234,
+ CONF_NAME: COVER_NAME,
+ CONF_ADDRESS: 1234,
CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
},
{
- CONF_NAME: "test2",
- CALL_TYPE_COIL: 1234,
+ CONF_NAME: f"{COVER_NAME}2",
+ CONF_INPUT_TYPE: CALL_TYPE_COIL,
+ CONF_ADDRESS: 1234,
},
]
}
@@ -274,24 +246,26 @@ async def test_service_cover_move(hass, mock_pymodbus):
config,
)
await hass.services.async_call(
- "cover", "open_cover", {"entity_id": entity_id}, blocking=True
+ "cover", "open_cover", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == STATE_OPEN
+ assert hass.states.get(ENTITY_ID).state == STATE_OPEN
mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00])
await hass.services.async_call(
- "cover", "close_cover", {"entity_id": entity_id}, blocking=True
+ "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == STATE_CLOSED
+ assert hass.states.get(ENTITY_ID).state == STATE_CLOSED
+ mock_pymodbus.reset()
mock_pymodbus.read_holding_registers.side_effect = ModbusException("fail write_")
await hass.services.async_call(
- "cover", "close_cover", {"entity_id": entity_id}, blocking=True
+ "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
+ assert mock_pymodbus.read_holding_registers.called
+ assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
mock_pymodbus.read_coils.side_effect = ModbusException("fail write_")
await hass.services.async_call(
- "cover", "close_cover", {"entity_id": entity_id2}, blocking=True
+ "cover", "close_cover", {"entity_id": ENTITY_ID2}, blocking=True
)
- assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE
+ assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE
diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py
index 2a9414d2277..4eeb094130b 100644
--- a/tests/components/modbus/test_fan.py
+++ b/tests/components/modbus/test_fan.py
@@ -32,84 +32,100 @@ from homeassistant.const import (
from homeassistant.core import State
from homeassistant.setup import async_setup_component
-from .conftest import ReadResult, base_config_test, base_test, prepare_service_update
+from .conftest import ReadResult, base_test, prepare_service_update
-from tests.common import mock_restore_cache
+FAN_NAME = "test_fan"
+ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}"
@pytest.mark.parametrize(
"do_config",
[
{
- CONF_ADDRESS: 1234,
+ CONF_FANS: [
+ {
+ CONF_NAME: FAN_NAME,
+ CONF_ADDRESS: 1234,
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_WRITE_TYPE: CALL_TYPE_COIL,
+ CONF_FANS: [
+ {
+ CONF_NAME: FAN_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_WRITE_TYPE: CALL_TYPE_COIL,
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- CONF_VERIFY: {
- CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
- CONF_ADDRESS: 1235,
- CONF_STATE_OFF: 0,
- CONF_STATE_ON: 1,
- },
+ CONF_FANS: [
+ {
+ CONF_NAME: FAN_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_VERIFY: {
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_ADDRESS: 1235,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ },
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- CONF_VERIFY: {
- CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
- CONF_ADDRESS: 1235,
- CONF_STATE_OFF: 0,
- CONF_STATE_ON: 1,
- },
+ CONF_FANS: [
+ {
+ CONF_NAME: FAN_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_VERIFY: {
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
+ CONF_ADDRESS: 1235,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ },
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- CONF_VERIFY: {
- CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
- CONF_ADDRESS: 1235,
- CONF_STATE_OFF: 0,
- CONF_STATE_ON: 1,
- },
+ CONF_FANS: [
+ {
+ CONF_NAME: FAN_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_VERIFY: {
+ CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
+ CONF_ADDRESS: 1235,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ },
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- CONF_VERIFY: None,
+ CONF_FANS: [
+ {
+ CONF_NAME: FAN_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_VERIFY: None,
+ }
+ ]
},
],
)
-async def test_config_fan(hass, do_config):
- """Run test for fan."""
- device_name = "test_fan"
-
- device_config = {
- CONF_NAME: device_name,
- **do_config,
- }
-
- await base_config_test(
- hass,
- device_config,
- device_name,
- FAN_DOMAIN,
- CONF_FANS,
- None,
- method_discovery=True,
- )
+async def test_config_fan(hass, mock_modbus):
+ """Run configuration test for fan."""
+ assert FAN_DOMAIN in hass.config.components
@pytest.mark.parametrize("call_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING])
@@ -145,17 +161,16 @@ async def test_config_fan(hass, do_config):
)
async def test_all_fan(hass, call_type, regs, verify, expected):
"""Run test for given config."""
- fan_name = "modbus_test_fan"
state = await base_test(
hass,
{
- CONF_NAME: fan_name,
+ CONF_NAME: FAN_NAME,
CONF_ADDRESS: 1234,
CONF_SLAVE: 1,
CONF_WRITE_TYPE: call_type,
**verify,
},
- fan_name,
+ FAN_NAME,
FAN_DOMAIN,
CONF_FANS,
None,
@@ -167,34 +182,33 @@ async def test_all_fan(hass, call_type, regs, verify, expected):
assert state == expected
-async def test_restore_state_fan(hass):
+@pytest.mark.parametrize(
+ "mock_test_state",
+ [(State(ENTITY_ID, STATE_ON),)],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "do_config",
+ [
+ {
+ CONF_FANS: [
+ {
+ CONF_NAME: FAN_NAME,
+ CONF_ADDRESS: 1234,
+ }
+ ]
+ },
+ ],
+)
+async def test_restore_state_fan(hass, mock_test_state, mock_modbus):
"""Run test for fan restore state."""
-
- fan_name = "test_fan"
- entity_id = f"{FAN_DOMAIN}.{fan_name}"
- test_value = STATE_ON
- config_fan = {CONF_NAME: fan_name, CONF_ADDRESS: 17}
- mock_restore_cache(
- hass,
- (State(f"{entity_id}", test_value),),
- )
- await base_config_test(
- hass,
- config_fan,
- fan_name,
- FAN_DOMAIN,
- CONF_FANS,
- None,
- method_discovery=True,
- )
- assert hass.states.get(entity_id).state == test_value
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
async def test_fan_service_turn(hass, caplog, mock_pymodbus):
"""Run test for service turn_on/turn_off."""
- entity_id1 = f"{FAN_DOMAIN}.fan1"
- entity_id2 = f"{FAN_DOMAIN}.fan2"
+ ENTITY_ID2 = f"{FAN_DOMAIN}.{FAN_NAME}2"
config = {
MODBUS_DOMAIN: {
CONF_TYPE: "tcp",
@@ -202,12 +216,12 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus):
CONF_PORT: 5501,
CONF_FANS: [
{
- CONF_NAME: "fan1",
+ CONF_NAME: FAN_NAME,
CONF_ADDRESS: 17,
CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING,
},
{
- CONF_NAME: "fan2",
+ CONF_NAME: f"{FAN_NAME}2",
CONF_ADDRESS: 17,
CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_VERIFY: {},
@@ -219,54 +233,53 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus):
await hass.async_block_till_done()
assert MODBUS_DOMAIN in hass.config.components
- assert hass.states.get(entity_id1).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
await hass.services.async_call(
- "fan", "turn_on", service_data={"entity_id": entity_id1}
+ "fan", "turn_on", service_data={"entity_id": ENTITY_ID}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id1).state == STATE_ON
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
await hass.services.async_call(
- "fan", "turn_off", service_data={"entity_id": entity_id1}
+ "fan", "turn_off", service_data={"entity_id": ENTITY_ID}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id1).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01])
- assert hass.states.get(entity_id2).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID2).state == STATE_OFF
await hass.services.async_call(
- "fan", "turn_on", service_data={"entity_id": entity_id2}
+ "fan", "turn_on", service_data={"entity_id": ENTITY_ID2}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id2).state == STATE_ON
+ assert hass.states.get(ENTITY_ID2).state == STATE_ON
mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00])
await hass.services.async_call(
- "fan", "turn_off", service_data={"entity_id": entity_id2}
+ "fan", "turn_off", service_data={"entity_id": ENTITY_ID2}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id2).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID2).state == STATE_OFF
mock_pymodbus.write_register.side_effect = ModbusException("fail write_")
await hass.services.async_call(
- "fan", "turn_on", service_data={"entity_id": entity_id2}
+ "fan", "turn_on", service_data={"entity_id": ENTITY_ID2}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE
+ assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE
mock_pymodbus.write_coil.side_effect = ModbusException("fail write_")
await hass.services.async_call(
- "fan", "turn_off", service_data={"entity_id": entity_id1}
+ "fan", "turn_off", service_data={"entity_id": ENTITY_ID}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id1).state == STATE_UNAVAILABLE
+ assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
async def test_service_fan_update(hass, mock_pymodbus):
"""Run test for service homeassistant.update_entity."""
- entity_id = "fan.test"
config = {
CONF_FANS: [
{
- CONF_NAME: "test",
+ CONF_NAME: FAN_NAME,
CONF_ADDRESS: 1234,
CONF_WRITE_TYPE: CALL_TYPE_COIL,
CONF_VERIFY: {},
@@ -279,11 +292,11 @@ async def test_service_fan_update(hass, mock_pymodbus):
config,
)
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == STATE_ON
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
mock_pymodbus.read_coils.return_value = ReadResult([0x00])
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py
index 0819e5a3e89..435b8446b6b 100644
--- a/tests/components/modbus/test_init.py
+++ b/tests/components/modbus/test_init.py
@@ -5,9 +5,12 @@ This file is responsible for testing:
- Functionality of class ModbusHub
- Coverage 100%:
__init__.py
- base_platform.py
const.py
modbus.py
+ validators.py
+ baseplatform.py (only BasePlatform)
+
+It uses binary_sensors/sensors to do black box testing of the read calls.
"""
from datetime import timedelta
import logging
@@ -19,7 +22,6 @@ import pytest
import voluptuous as vol
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
-from homeassistant.components.modbus import number
from homeassistant.components.modbus.const import (
ATTR_ADDRESS,
ATTR_HUB,
@@ -36,18 +38,30 @@ from homeassistant.components.modbus.const import (
CALL_TYPE_WRITE_REGISTERS,
CONF_BAUDRATE,
CONF_BYTESIZE,
+ CONF_DATA_TYPE,
CONF_INPUT_TYPE,
CONF_PARITY,
CONF_STOPBITS,
+ CONF_SWAP,
+ CONF_SWAP_BYTE,
+ CONF_SWAP_WORD,
+ DATA_TYPE_CUSTOM,
+ DATA_TYPE_INT,
+ DATA_TYPE_STRING,
DEFAULT_SCAN_INTERVAL,
MODBUS_DOMAIN as DOMAIN,
SERVICE_WRITE_COIL,
SERVICE_WRITE_REGISTER,
)
+from homeassistant.components.modbus.validators import (
+ number_validator,
+ sensor_schema_validator,
+)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import (
CONF_ADDRESS,
CONF_BINARY_SENSORS,
+ CONF_COUNT,
CONF_DELAY,
CONF_HOST,
CONF_METHOD,
@@ -55,6 +69,7 @@ from homeassistant.const import (
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_SENSORS,
+ CONF_STRUCTURE,
CONF_TIMEOUT,
CONF_TYPE,
STATE_ON,
@@ -63,13 +78,27 @@ from homeassistant.const import (
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
-from .conftest import TEST_MODBUS_NAME, ReadResult
+from .conftest import ReadResult
from tests.common import async_fire_time_changed
TEST_SENSOR_NAME = "testSensor"
TEST_ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}"
TEST_HOST = "modbusTestHost"
+TEST_MODBUS_NAME = "modbusTest"
+
+
+@pytest.fixture
+async def mock_modbus_with_pymodbus(hass, caplog, do_config, mock_pymodbus):
+ """Load integration modbus using mocked pymodbus."""
+ caplog.clear()
+ caplog.set_level(logging.ERROR)
+ config = {DOMAIN: do_config}
+ assert await async_setup_component(hass, DOMAIN, config) is True
+ await hass.async_block_till_done()
+ assert DOMAIN in hass.config.components
+ assert caplog.text == ""
+ yield mock_pymodbus
async def test_number_validator():
@@ -85,13 +114,84 @@ async def test_number_validator():
("-15", int),
("-15.1", float),
]:
- assert isinstance(number(value), value_type)
+ assert isinstance(number_validator(value), value_type)
try:
- number("x15.1")
+ number_validator("x15.1")
except (vol.Invalid):
return
- pytest.fail("Number not throwing exception")
+ pytest.fail("Number_validator not throwing exception")
+
+
+@pytest.mark.parametrize(
+ "do_config",
+ [
+ {
+ CONF_NAME: TEST_SENSOR_NAME,
+ CONF_COUNT: 2,
+ CONF_DATA_TYPE: DATA_TYPE_STRING,
+ },
+ {
+ CONF_NAME: TEST_SENSOR_NAME,
+ CONF_COUNT: 2,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ },
+ {
+ CONF_NAME: TEST_SENSOR_NAME,
+ CONF_COUNT: 2,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ CONF_SWAP: CONF_SWAP_BYTE,
+ },
+ ],
+)
+async def test_ok_sensor_schema_validator(do_config):
+ """Test struct validator."""
+ try:
+ sensor_schema_validator(do_config)
+ except vol.Invalid:
+ pytest.fail("Sensor_schema_validator unexpected exception")
+
+
+@pytest.mark.parametrize(
+ "do_config",
+ [
+ {
+ CONF_NAME: TEST_SENSOR_NAME,
+ CONF_COUNT: 8,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ },
+ {
+ CONF_NAME: TEST_SENSOR_NAME,
+ CONF_COUNT: 8,
+ CONF_DATA_TYPE: DATA_TYPE_CUSTOM,
+ },
+ {
+ CONF_NAME: TEST_SENSOR_NAME,
+ CONF_COUNT: 8,
+ CONF_DATA_TYPE: DATA_TYPE_CUSTOM,
+ CONF_STRUCTURE: "no good",
+ },
+ {
+ CONF_NAME: TEST_SENSOR_NAME,
+ CONF_COUNT: 20,
+ CONF_DATA_TYPE: DATA_TYPE_CUSTOM,
+ CONF_STRUCTURE: ">f",
+ },
+ {
+ CONF_NAME: TEST_SENSOR_NAME,
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ CONF_SWAP: CONF_SWAP_WORD,
+ },
+ ],
+)
+async def test_exception_sensor_schema_validator(do_config):
+ """Test struct validator."""
+ try:
+ sensor_schema_validator(do_config)
+ except vol.Invalid:
+ return
+ pytest.fail("Sensor_schema_validator missing exception")
@pytest.mark.parametrize(
@@ -187,16 +287,23 @@ async def test_number_validator():
CONF_NAME: TEST_MODBUS_NAME + "3",
},
],
+ {
+ # Special test for scan_interval validator with scan_interval: 0
+ CONF_TYPE: "tcp",
+ CONF_HOST: TEST_HOST,
+ CONF_PORT: 5501,
+ CONF_SENSORS: [
+ {
+ CONF_NAME: TEST_SENSOR_NAME,
+ CONF_ADDRESS: 117,
+ CONF_SCAN_INTERVAL: 0,
+ }
+ ],
+ },
],
)
-async def test_config_modbus(hass, caplog, do_config, mock_pymodbus):
+async def test_config_modbus(hass, caplog, mock_modbus_with_pymodbus):
"""Run configuration test for modbus."""
- config = {DOMAIN: do_config}
- caplog.set_level(logging.ERROR)
- assert await async_setup_component(hass, DOMAIN, config) is True
- await hass.async_block_till_done()
- assert DOMAIN in hass.config.components
- assert len(caplog.records) == 0
VALUE = "value"
@@ -205,6 +312,21 @@ DATA = "data"
SERVICE = "service"
+@pytest.mark.parametrize(
+ "do_config",
+ [
+ {
+ CONF_NAME: TEST_MODBUS_NAME,
+ CONF_TYPE: "serial",
+ CONF_BAUDRATE: 9600,
+ CONF_BYTESIZE: 8,
+ CONF_METHOD: "rtu",
+ CONF_PORT: "usb01",
+ CONF_PARITY: "E",
+ CONF_STOPBITS: 1,
+ },
+ ],
+)
@pytest.mark.parametrize(
"do_write",
[
@@ -234,14 +356,25 @@ SERVICE = "service"
},
],
)
-async def test_pb_service_write(hass, do_write, caplog, mock_modbus):
+@pytest.mark.parametrize(
+ "do_return",
+ [
+ {VALUE: ReadResult([0x0001]), DATA: ""},
+ {VALUE: ExceptionResponse(0x06), DATA: "Pymodbus:"},
+ {VALUE: IllegalFunctionRequest(0x06), DATA: "Pymodbus:"},
+ {VALUE: ModbusException("fail write_"), DATA: "Pymodbus:"},
+ ],
+)
+async def test_pb_service_write(
+ hass, do_write, do_return, caplog, mock_modbus_with_pymodbus
+):
"""Run test for service write_register."""
func_name = {
- CALL_TYPE_WRITE_COIL: mock_modbus.write_coil,
- CALL_TYPE_WRITE_COILS: mock_modbus.write_coils,
- CALL_TYPE_WRITE_REGISTER: mock_modbus.write_register,
- CALL_TYPE_WRITE_REGISTERS: mock_modbus.write_registers,
+ CALL_TYPE_WRITE_COIL: mock_modbus_with_pymodbus.write_coil,
+ CALL_TYPE_WRITE_COILS: mock_modbus_with_pymodbus.write_coils,
+ CALL_TYPE_WRITE_REGISTER: mock_modbus_with_pymodbus.write_register,
+ CALL_TYPE_WRITE_REGISTERS: mock_modbus_with_pymodbus.write_registers,
}
data = {
@@ -250,28 +383,42 @@ async def test_pb_service_write(hass, do_write, caplog, mock_modbus):
ATTR_ADDRESS: 16,
do_write[DATA]: do_write[VALUE],
}
+ mock_modbus_with_pymodbus.reset_mock()
+ caplog.clear()
+ caplog.set_level(logging.DEBUG)
+ func_name[do_write[FUNC]].return_value = do_return[VALUE]
await hass.services.async_call(DOMAIN, do_write[SERVICE], data, blocking=True)
assert func_name[do_write[FUNC]].called
assert func_name[do_write[FUNC]].call_args[0] == (
data[ATTR_ADDRESS],
data[do_write[DATA]],
)
- mock_modbus.reset_mock()
-
- for return_value in [
- ExceptionResponse(0x06),
- IllegalFunctionRequest(0x06),
- ModbusException("fail write_"),
- ]:
- caplog.set_level(logging.DEBUG)
- func_name[do_write[FUNC]].return_value = return_value
- await hass.services.async_call(DOMAIN, do_write[SERVICE], data, blocking=True)
- assert func_name[do_write[FUNC]].called
+ if do_return[DATA]:
assert caplog.messages[-1].startswith("Pymodbus:")
- mock_modbus.reset_mock()
-async def _read_helper(hass, do_group, do_type, do_return, do_exception, mock_pymodbus):
+@pytest.fixture
+async def mock_modbus_read_pymodbus(
+ hass,
+ do_group,
+ do_type,
+ do_scan_interval,
+ do_return,
+ do_exception,
+ caplog,
+ mock_pymodbus,
+):
+ """Load integration modbus using mocked pymodbus."""
+ caplog.clear()
+ caplog.set_level(logging.ERROR)
+ mock_pymodbus.read_coils.side_effect = do_exception
+ mock_pymodbus.read_discrete_inputs.side_effect = do_exception
+ mock_pymodbus.read_input_registers.side_effect = do_exception
+ mock_pymodbus.read_holding_registers.side_effect = do_exception
+ mock_pymodbus.read_coils.return_value = do_return
+ mock_pymodbus.read_discrete_inputs.return_value = do_return
+ mock_pymodbus.read_input_registers.return_value = do_return
+ mock_pymodbus.read_holding_registers.return_value = do_return
config = {
DOMAIN: [
{
@@ -284,91 +431,63 @@ async def _read_helper(hass, do_group, do_type, do_return, do_exception, mock_py
CONF_INPUT_TYPE: do_type,
CONF_NAME: TEST_SENSOR_NAME,
CONF_ADDRESS: 51,
- CONF_SCAN_INTERVAL: 1,
+ CONF_SCAN_INTERVAL: do_scan_interval,
}
],
}
- ]
+ ],
}
- mock_pymodbus.read_coils.side_effect = do_exception
- mock_pymodbus.read_discrete_inputs.side_effect = do_exception
- mock_pymodbus.read_input_registers.side_effect = do_exception
- mock_pymodbus.read_holding_registers.side_effect = do_exception
- mock_pymodbus.read_coils.return_value = do_return
- mock_pymodbus.read_discrete_inputs.return_value = do_return
- mock_pymodbus.read_input_registers.return_value = do_return
- mock_pymodbus.read_holding_registers.return_value = do_return
now = dt_util.utcnow()
with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
assert await async_setup_component(hass, DOMAIN, config) is True
await hass.async_block_till_done()
+ assert DOMAIN in hass.config.components
+ assert caplog.text == ""
now = now + timedelta(seconds=DEFAULT_SCAN_INTERVAL + 60)
with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
async_fire_time_changed(hass, now)
await hass.async_block_till_done()
+ yield mock_pymodbus
@pytest.mark.parametrize(
- "do_return,do_exception,do_expect",
+ "do_domain, do_group,do_type,do_scan_interval",
[
- [ReadResult([7]), None, "7"],
- [IllegalFunctionRequest(0x99), None, STATE_UNAVAILABLE],
- [ExceptionResponse(0x99), None, STATE_UNAVAILABLE],
- [ReadResult([7]), ModbusException("fail read_"), STATE_UNAVAILABLE],
+ [SENSOR_DOMAIN, CONF_SENSORS, CALL_TYPE_REGISTER_HOLDING, 10],
+ [SENSOR_DOMAIN, CONF_SENSORS, CALL_TYPE_REGISTER_INPUT, 10],
+ [BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS, CALL_TYPE_DISCRETE, 10],
+ [BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS, CALL_TYPE_COIL, 1],
],
)
@pytest.mark.parametrize(
- "do_type",
- [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT],
+ "do_return,do_exception,do_expect_state,do_expect_value",
+ [
+ [ReadResult([1]), None, STATE_ON, "1"],
+ [IllegalFunctionRequest(0x99), None, STATE_UNAVAILABLE, STATE_UNAVAILABLE],
+ [ExceptionResponse(0x99), None, STATE_UNAVAILABLE, STATE_UNAVAILABLE],
+ [
+ ReadResult([1]),
+ ModbusException("fail read_"),
+ STATE_UNAVAILABLE,
+ STATE_UNAVAILABLE,
+ ],
+ ],
)
-async def test_pb_read_value(
- hass, caplog, do_type, do_return, do_exception, do_expect, mock_pymodbus
+async def test_pb_read(
+ hass, do_domain, do_expect_state, do_expect_value, caplog, mock_modbus_read_pymodbus
):
"""Run test for different read."""
- # the purpose of this test is to test the special
- # return values from pymodbus:
- # ExceptionResponse, IllegalResponse
- # and exceptions.
- # We "hijiack" binary_sensor and sensor in order
- # to make a proper blackbox test.
- await _read_helper(
- hass, CONF_SENSORS, do_type, do_return, do_exception, mock_pymodbus
- )
-
# Check state
- entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}"
+ entity_id = f"{do_domain}.{TEST_SENSOR_NAME}"
+ state = hass.states.get(entity_id).state
assert hass.states.get(entity_id).state
-
-@pytest.mark.parametrize(
- "do_return,do_exception,do_expect",
- [
- [ReadResult([0x01]), None, STATE_ON],
- [IllegalFunctionRequest(0x99), None, STATE_UNAVAILABLE],
- [ExceptionResponse(0x99), None, STATE_UNAVAILABLE],
- [ReadResult([7]), ModbusException("fail read_"), STATE_UNAVAILABLE],
- ],
-)
-@pytest.mark.parametrize("do_type", [CALL_TYPE_DISCRETE, CALL_TYPE_COIL])
-async def test_pb_read_state(
- hass, caplog, do_type, do_return, do_exception, do_expect, mock_pymodbus
-):
- """Run test for different read."""
-
- # the purpose of this test is to test the special
- # return values from pymodbus:
- # ExceptionResponse, IllegalResponse
- # and exceptions.
- # We "hijiack" binary_sensor and sensor in order
- # to make a proper blackbox test.
- await _read_helper(
- hass, CONF_BINARY_SENSORS, do_type, do_return, do_exception, mock_pymodbus
- )
-
- # Check state
- entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}"
- state = hass.states.get(entity_id).state
+ # this if is needed to avoid explode the
+ if do_domain == SENSOR_DOMAIN:
+ do_expect = do_expect_value
+ else:
+ do_expect = do_expect_state
assert state == do_expect
@@ -388,15 +507,24 @@ async def test_pymodbus_constructor_fail(hass, caplog):
) as mock_pb:
caplog.set_level(logging.ERROR)
mock_pb.side_effect = ModbusException("test no class")
- assert await async_setup_component(hass, DOMAIN, config) is True
+ assert await async_setup_component(hass, DOMAIN, config) is False
await hass.async_block_till_done()
- assert len(caplog.records) == 1
+ assert caplog.messages[0].startswith("Pymodbus: Modbus Error: test")
assert caplog.records[0].levelname == "ERROR"
assert mock_pb.called
-async def test_pymodbus_connect_fail(hass, caplog, mock_pymodbus):
- """Run test for failing pymodbus constructor."""
+@pytest.mark.parametrize(
+ "do_connect,do_exception,do_text",
+ [
+ [False, None, "initial connect failed, no retry"],
+ [True, ModbusException("no connect"), "Modbus Error: no connect"],
+ ],
+)
+async def test_pymodbus_connect_fail(
+ hass, do_connect, do_exception, do_text, caplog, mock_pymodbus
+):
+ """Run test for failing pymodbus connect."""
config = {
DOMAIN: [
{
@@ -407,12 +535,69 @@ async def test_pymodbus_connect_fail(hass, caplog, mock_pymodbus):
]
}
caplog.set_level(logging.ERROR)
- mock_pymodbus.connect.side_effect = ModbusException("test connect fail")
- mock_pymodbus.close.side_effect = ModbusException("test connect fail")
+ mock_pymodbus.connect.return_value = do_connect
+ mock_pymodbus.connect.side_effect = do_exception
+ assert await async_setup_component(hass, DOMAIN, config) is False
+ await hass.async_block_till_done()
+ assert caplog.messages[0].startswith(f"Pymodbus: {do_text}")
+ assert caplog.records[0].levelname == "ERROR"
+
+
+async def test_pymodbus_close_fail(hass, caplog, mock_pymodbus):
+ """Run test for failing pymodbus close."""
+ config = {
+ DOMAIN: [
+ {
+ CONF_TYPE: "tcp",
+ CONF_HOST: TEST_HOST,
+ CONF_PORT: 5501,
+ }
+ ]
+ }
+ caplog.set_level(logging.ERROR)
+ mock_pymodbus.connect.return_value = True
+ mock_pymodbus.close.side_effect = ModbusException("close fail")
assert await async_setup_component(hass, DOMAIN, config) is True
await hass.async_block_till_done()
- assert len(caplog.records) == 1
- assert caplog.records[0].levelname == "ERROR"
+ # Close() is called as part of teardown
+
+
+async def test_disconnect(hass, mock_pymodbus):
+ """Run test for startup delay."""
+
+ # the purpose of this test is to test a device disconnect
+ # We "hijiack" a binary_sensor to make a proper blackbox test.
+ entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}"
+ config = {
+ DOMAIN: [
+ {
+ CONF_TYPE: "tcp",
+ CONF_HOST: TEST_HOST,
+ CONF_PORT: 5501,
+ CONF_NAME: TEST_MODBUS_NAME,
+ CONF_BINARY_SENSORS: [
+ {
+ CONF_INPUT_TYPE: CALL_TYPE_COIL,
+ CONF_NAME: f"{TEST_SENSOR_NAME}",
+ CONF_ADDRESS: 52,
+ },
+ ],
+ }
+ ]
+ }
+ mock_pymodbus.read_coils.return_value = ReadResult([0x01])
+ mock_pymodbus.is_socket_open.return_value = False
+ now = dt_util.utcnow()
+ with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
+ assert await async_setup_component(hass, DOMAIN, config) is True
+ await hass.async_block_till_done()
+
+ # pass first scan_interval
+ now = now + timedelta(seconds=20)
+ 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_UNAVAILABLE
async def test_delay(hass, mock_pymodbus):
diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py
index 12e72e54155..e962b69a2a6 100644
--- a/tests/components/modbus/test_light.py
+++ b/tests/components/modbus/test_light.py
@@ -32,84 +32,100 @@ from homeassistant.const import (
from homeassistant.core import State
from homeassistant.setup import async_setup_component
-from .conftest import ReadResult, base_config_test, base_test, prepare_service_update
+from .conftest import ReadResult, base_test, prepare_service_update
-from tests.common import mock_restore_cache
+LIGHT_NAME = "test_light"
+ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}"
@pytest.mark.parametrize(
"do_config",
[
{
- CONF_ADDRESS: 1234,
+ CONF_LIGHTS: [
+ {
+ CONF_NAME: LIGHT_NAME,
+ CONF_ADDRESS: 1234,
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_WRITE_TYPE: CALL_TYPE_COIL,
+ CONF_LIGHTS: [
+ {
+ CONF_NAME: LIGHT_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_WRITE_TYPE: CALL_TYPE_COIL,
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- CONF_VERIFY: {
- CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
- CONF_ADDRESS: 1235,
- CONF_STATE_OFF: 0,
- CONF_STATE_ON: 1,
- },
+ CONF_LIGHTS: [
+ {
+ CONF_NAME: LIGHT_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_VERIFY: {
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_ADDRESS: 1235,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ },
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- CONF_VERIFY: {
- CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
- CONF_ADDRESS: 1235,
- CONF_STATE_OFF: 0,
- CONF_STATE_ON: 1,
- },
+ CONF_LIGHTS: [
+ {
+ CONF_NAME: LIGHT_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_VERIFY: {
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
+ CONF_ADDRESS: 1235,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ },
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- CONF_VERIFY: {
- CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
- CONF_ADDRESS: 1235,
- CONF_STATE_OFF: 0,
- CONF_STATE_ON: 1,
- },
+ CONF_LIGHTS: [
+ {
+ CONF_NAME: LIGHT_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_VERIFY: {
+ CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
+ CONF_ADDRESS: 1235,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ },
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- CONF_VERIFY: None,
+ CONF_LIGHTS: [
+ {
+ CONF_NAME: LIGHT_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_VERIFY: None,
+ }
+ ]
},
],
)
-async def test_config_light(hass, do_config):
- """Run test for light."""
- device_name = "test_light"
-
- device_config = {
- CONF_NAME: device_name,
- **do_config,
- }
-
- await base_config_test(
- hass,
- device_config,
- device_name,
- LIGHT_DOMAIN,
- CONF_LIGHTS,
- None,
- method_discovery=True,
- )
+async def test_config_light(hass, mock_modbus):
+ """Run configuration test for light."""
+ assert LIGHT_DOMAIN in hass.config.components
@pytest.mark.parametrize("call_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING])
@@ -145,17 +161,16 @@ async def test_config_light(hass, do_config):
)
async def test_all_light(hass, call_type, regs, verify, expected):
"""Run test for given config."""
- light_name = "modbus_test_light"
state = await base_test(
hass,
{
- CONF_NAME: light_name,
+ CONF_NAME: LIGHT_NAME,
CONF_ADDRESS: 1234,
CONF_SLAVE: 1,
CONF_WRITE_TYPE: call_type,
**verify,
},
- light_name,
+ LIGHT_NAME,
LIGHT_DOMAIN,
CONF_LIGHTS,
None,
@@ -167,34 +182,33 @@ async def test_all_light(hass, call_type, regs, verify, expected):
assert state == expected
-async def test_restore_state_light(hass):
+@pytest.mark.parametrize(
+ "mock_test_state",
+ [(State(ENTITY_ID, STATE_ON),)],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "do_config",
+ [
+ {
+ CONF_LIGHTS: [
+ {
+ CONF_NAME: LIGHT_NAME,
+ CONF_ADDRESS: 1234,
+ }
+ ]
+ },
+ ],
+)
+async def test_restore_state_light(hass, mock_test_state, mock_modbus):
"""Run test for sensor restore state."""
-
- light_name = "test_light"
- entity_id = f"{LIGHT_DOMAIN}.{light_name}"
- test_value = STATE_ON
- config_light = {CONF_NAME: light_name, CONF_ADDRESS: 17}
- mock_restore_cache(
- hass,
- (State(f"{entity_id}", test_value),),
- )
- await base_config_test(
- hass,
- config_light,
- light_name,
- LIGHT_DOMAIN,
- CONF_LIGHTS,
- None,
- method_discovery=True,
- )
- assert hass.states.get(entity_id).state == test_value
+ assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state
async def test_light_service_turn(hass, caplog, mock_pymodbus):
"""Run test for service turn_on/turn_off."""
- entity_id1 = f"{LIGHT_DOMAIN}.light1"
- entity_id2 = f"{LIGHT_DOMAIN}.light2"
+ ENTITY_ID2 = f"{ENTITY_ID}2"
config = {
MODBUS_DOMAIN: {
CONF_TYPE: "tcp",
@@ -202,12 +216,12 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus):
CONF_PORT: 5501,
CONF_LIGHTS: [
{
- CONF_NAME: "light1",
+ CONF_NAME: LIGHT_NAME,
CONF_ADDRESS: 17,
CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING,
},
{
- CONF_NAME: "light2",
+ CONF_NAME: f"{LIGHT_NAME}2",
CONF_ADDRESS: 17,
CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_VERIFY: {},
@@ -219,54 +233,53 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus):
await hass.async_block_till_done()
assert MODBUS_DOMAIN in hass.config.components
- assert hass.states.get(entity_id1).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
await hass.services.async_call(
- "light", "turn_on", service_data={"entity_id": entity_id1}
+ "light", "turn_on", service_data={"entity_id": ENTITY_ID}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id1).state == STATE_ON
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
await hass.services.async_call(
- "light", "turn_off", service_data={"entity_id": entity_id1}
+ "light", "turn_off", service_data={"entity_id": ENTITY_ID}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id1).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01])
- assert hass.states.get(entity_id2).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID2).state == STATE_OFF
await hass.services.async_call(
- "light", "turn_on", service_data={"entity_id": entity_id2}
+ "light", "turn_on", service_data={"entity_id": ENTITY_ID2}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id2).state == STATE_ON
+ assert hass.states.get(ENTITY_ID2).state == STATE_ON
mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00])
await hass.services.async_call(
- "light", "turn_off", service_data={"entity_id": entity_id2}
+ "light", "turn_off", service_data={"entity_id": ENTITY_ID2}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id2).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID2).state == STATE_OFF
mock_pymodbus.write_register.side_effect = ModbusException("fail write_")
await hass.services.async_call(
- "light", "turn_on", service_data={"entity_id": entity_id2}
+ "light", "turn_on", service_data={"entity_id": ENTITY_ID2}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE
+ assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE
mock_pymodbus.write_coil.side_effect = ModbusException("fail write_")
await hass.services.async_call(
- "light", "turn_off", service_data={"entity_id": entity_id1}
+ "light", "turn_off", service_data={"entity_id": ENTITY_ID}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id1).state == STATE_UNAVAILABLE
+ assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
async def test_service_light_update(hass, mock_pymodbus):
"""Run test for service homeassistant.update_entity."""
- entity_id = "light.test"
config = {
CONF_LIGHTS: [
{
- CONF_NAME: "test",
+ CONF_NAME: LIGHT_NAME,
CONF_ADDRESS: 1234,
CONF_WRITE_TYPE: CALL_TYPE_COIL,
CONF_VERIFY: {},
@@ -279,11 +292,11 @@ async def test_service_light_update(hass, mock_pymodbus):
config,
)
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == STATE_ON
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
mock_pymodbus.read_coils.return_value = ReadResult([0x00])
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_sensor.py
similarity index 65%
rename from tests/components/modbus/test_modbus_sensor.py
rename to tests/components/modbus/test_sensor.py
index cb784ac46b3..f9bc8454281 100644
--- a/tests/components/modbus/test_modbus_sensor.py
+++ b/tests/components/modbus/test_sensor.py
@@ -9,10 +9,7 @@ from homeassistant.components.modbus.const import (
CONF_DATA_TYPE,
CONF_INPUT_TYPE,
CONF_PRECISION,
- CONF_REGISTER,
- CONF_REGISTER_TYPE,
CONF_REGISTERS,
- CONF_REVERSE_ORDER,
CONF_SCALE,
CONF_SWAP,
CONF_SWAP_BYTE,
@@ -41,179 +38,199 @@ from homeassistant.core import State
from .conftest import ReadResult, base_config_test, base_test, prepare_service_update
-from tests.common import mock_restore_cache
-
-
-@pytest.mark.parametrize(
- "do_discovery, do_config",
- [
- (
- False,
- {
- CONF_REGISTER: 51,
- },
- ),
- (
- False,
- {
- CONF_REGISTER: 51,
- CONF_SLAVE: 10,
- CONF_COUNT: 1,
- CONF_DATA_TYPE: "int",
- CONF_PRECISION: 0,
- CONF_SCALE: 1,
- CONF_REVERSE_ORDER: False,
- CONF_OFFSET: 0,
- CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
- CONF_DEVICE_CLASS: "battery",
- },
- ),
- (
- False,
- {
- CONF_REGISTER: 51,
- CONF_SLAVE: 10,
- CONF_COUNT: 1,
- CONF_DATA_TYPE: "int",
- CONF_PRECISION: 0,
- CONF_SCALE: 1,
- CONF_REVERSE_ORDER: False,
- CONF_OFFSET: 0,
- CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT,
- CONF_DEVICE_CLASS: "battery",
- },
- ),
- (
- True,
- {
- CONF_ADDRESS: 51,
- },
- ),
- (
- True,
- {
- CONF_ADDRESS: 51,
- CONF_SLAVE: 10,
- CONF_COUNT: 1,
- CONF_DATA_TYPE: "int",
- CONF_PRECISION: 0,
- CONF_SCALE: 1,
- CONF_REVERSE_ORDER: False,
- CONF_OFFSET: 0,
- CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
- CONF_DEVICE_CLASS: "battery",
- },
- ),
- (
- True,
- {
- CONF_ADDRESS: 51,
- CONF_SLAVE: 10,
- CONF_COUNT: 1,
- CONF_DATA_TYPE: "int",
- CONF_PRECISION: 0,
- CONF_SCALE: 1,
- CONF_REVERSE_ORDER: False,
- CONF_OFFSET: 0,
- CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
- CONF_DEVICE_CLASS: "battery",
- },
- ),
- (
- True,
- {
- CONF_ADDRESS: 51,
- CONF_COUNT: 1,
- CONF_SWAP: CONF_SWAP_NONE,
- },
- ),
- (
- True,
- {
- CONF_ADDRESS: 51,
- CONF_COUNT: 1,
- CONF_SWAP: CONF_SWAP_BYTE,
- },
- ),
- (
- True,
- {
- CONF_ADDRESS: 51,
- CONF_COUNT: 2,
- CONF_SWAP: CONF_SWAP_WORD,
- },
- ),
- (
- True,
- {
- CONF_ADDRESS: 51,
- CONF_COUNT: 2,
- CONF_SWAP: CONF_SWAP_WORD_BYTE,
- },
- ),
- ],
-)
-async def test_config_sensor(hass, do_discovery, do_config):
- """Run test for sensor."""
- sensor_name = "test_sensor"
- config_sensor = {
- CONF_NAME: sensor_name,
- **do_config,
- }
- await base_config_test(
- hass,
- config_sensor,
- sensor_name,
- SENSOR_DOMAIN,
- CONF_SENSORS,
- CONF_REGISTERS,
- method_discovery=do_discovery,
- )
+SENSOR_NAME = "test_sensor"
+ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}"
@pytest.mark.parametrize(
"do_config",
[
{
- CONF_ADDRESS: 1234,
- CONF_COUNT: 8,
- CONF_PRECISION: 2,
- CONF_DATA_TYPE: DATA_TYPE_INT,
+ CONF_SENSORS: [
+ {
+ CONF_NAME: SENSOR_NAME,
+ CONF_ADDRESS: 51,
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_COUNT: 8,
- CONF_PRECISION: 2,
- CONF_DATA_TYPE: DATA_TYPE_CUSTOM,
- CONF_STRUCTURE: ">no struct",
+ CONF_SENSORS: [
+ {
+ CONF_NAME: SENSOR_NAME,
+ CONF_ADDRESS: 51,
+ CONF_SLAVE: 10,
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: "int",
+ CONF_PRECISION: 0,
+ CONF_SCALE: 1,
+ CONF_OFFSET: 0,
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_DEVICE_CLASS: "battery",
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_COUNT: 2,
- CONF_PRECISION: 2,
- CONF_DATA_TYPE: DATA_TYPE_CUSTOM,
- CONF_STRUCTURE: ">4f",
+ CONF_SENSORS: [
+ {
+ CONF_NAME: SENSOR_NAME,
+ CONF_ADDRESS: 51,
+ CONF_SLAVE: 10,
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: "int",
+ CONF_PRECISION: 0,
+ CONF_SCALE: 1,
+ CONF_OFFSET: 0,
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
+ CONF_DEVICE_CLASS: "battery",
+ }
+ ]
+ },
+ {
+ CONF_SENSORS: [
+ {
+ CONF_NAME: SENSOR_NAME,
+ CONF_ADDRESS: 51,
+ CONF_COUNT: 1,
+ CONF_SWAP: CONF_SWAP_NONE,
+ }
+ ]
+ },
+ {
+ CONF_SENSORS: [
+ {
+ CONF_NAME: SENSOR_NAME,
+ CONF_ADDRESS: 51,
+ CONF_COUNT: 1,
+ CONF_SWAP: CONF_SWAP_BYTE,
+ }
+ ]
+ },
+ {
+ CONF_SENSORS: [
+ {
+ CONF_NAME: SENSOR_NAME,
+ CONF_ADDRESS: 51,
+ CONF_COUNT: 2,
+ CONF_SWAP: CONF_SWAP_WORD,
+ }
+ ]
+ },
+ {
+ CONF_SENSORS: [
+ {
+ CONF_NAME: SENSOR_NAME,
+ CONF_ADDRESS: 51,
+ CONF_COUNT: 2,
+ CONF_SWAP: CONF_SWAP_WORD_BYTE,
+ }
+ ]
},
],
)
-async def test_config_wrong_struct_sensor(hass, do_config):
+async def test_config_sensor(hass, mock_modbus):
+ """Run configuration test for sensor."""
+ assert SENSOR_DOMAIN in hass.config.components
+
+
+@pytest.mark.parametrize(
+ "do_config,error_message",
+ [
+ (
+ {
+ CONF_ADDRESS: 1234,
+ CONF_COUNT: 8,
+ CONF_PRECISION: 2,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ },
+ "Unable to detect data type for test_sensor sensor, try a custom type",
+ ),
+ (
+ {
+ CONF_ADDRESS: 1234,
+ CONF_COUNT: 8,
+ CONF_PRECISION: 2,
+ CONF_DATA_TYPE: DATA_TYPE_CUSTOM,
+ CONF_STRUCTURE: ">no struct",
+ },
+ "Error in sensor test_sensor structure: bad char in struct format",
+ ),
+ (
+ {
+ CONF_ADDRESS: 1234,
+ CONF_COUNT: 2,
+ CONF_PRECISION: 2,
+ CONF_DATA_TYPE: DATA_TYPE_CUSTOM,
+ CONF_STRUCTURE: ">4f",
+ },
+ "Structure request 16 bytes, but 2 registers have a size of 4 bytes",
+ ),
+ (
+ {
+ CONF_ADDRESS: 1234,
+ CONF_DATA_TYPE: DATA_TYPE_CUSTOM,
+ CONF_COUNT: 4,
+ CONF_SWAP: CONF_SWAP_NONE,
+ CONF_STRUCTURE: "invalid",
+ },
+ "Error in sensor test_sensor structure: bad char in struct format",
+ ),
+ (
+ {
+ CONF_ADDRESS: 1234,
+ CONF_DATA_TYPE: DATA_TYPE_CUSTOM,
+ CONF_COUNT: 4,
+ CONF_SWAP: CONF_SWAP_NONE,
+ CONF_STRUCTURE: "",
+ },
+ "Error in sensor test_sensor. The `structure` field can not be empty if the parameter `data_type` is set to the `custom`",
+ ),
+ (
+ {
+ CONF_ADDRESS: 1234,
+ CONF_DATA_TYPE: DATA_TYPE_CUSTOM,
+ CONF_COUNT: 4,
+ CONF_SWAP: CONF_SWAP_NONE,
+ CONF_STRUCTURE: "1s",
+ },
+ "Structure request 1 bytes, but 4 registers have a size of 8 bytes",
+ ),
+ (
+ {
+ CONF_ADDRESS: 1234,
+ CONF_DATA_TYPE: DATA_TYPE_CUSTOM,
+ CONF_COUNT: 1,
+ CONF_STRUCTURE: "2s",
+ CONF_SWAP: CONF_SWAP_WORD,
+ },
+ "Error in sensor test_sensor swap(word) not possible due to the registers count: 1, needed: 2",
+ ),
+ ],
+)
+async def test_config_wrong_struct_sensor(
+ hass, caplog, do_config, error_message, mock_pymodbus
+):
"""Run test for sensor with wrong struct."""
- sensor_name = "test_sensor"
config_sensor = {
- CONF_NAME: sensor_name,
+ CONF_NAME: SENSOR_NAME,
**do_config,
}
+ caplog.set_level(logging.WARNING)
+ caplog.clear()
+
await base_config_test(
hass,
config_sensor,
- sensor_name,
+ SENSOR_NAME,
SENSOR_DOMAIN,
CONF_SENSORS,
None,
method_discovery=True,
+ expect_setup_to_fail=True,
)
+ assert error_message in caplog.text
+
@pytest.mark.parametrize(
"cfg,regs,expected",
@@ -333,15 +350,6 @@ async def test_config_wrong_struct_sensor(hass, do_config):
[0x89AB, 0xCDEF],
str(0x89ABCDEF),
),
- (
- {
- CONF_COUNT: 2,
- CONF_DATA_TYPE: DATA_TYPE_UINT,
- CONF_REVERSE_ORDER: True,
- },
- [0x89AB, 0xCDEF],
- str(0xCDEF89AB),
- ),
(
{
CONF_COUNT: 4,
@@ -497,11 +505,10 @@ async def test_config_wrong_struct_sensor(hass, do_config):
async def test_all_sensor(hass, cfg, regs, expected):
"""Run test for sensor."""
- sensor_name = "modbus_test_sensor"
state = await base_test(
hass,
- {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, **cfg},
- sensor_name,
+ {CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, **cfg},
+ SENSOR_NAME,
SENSOR_DOMAIN,
CONF_SENSORS,
CONF_REGISTERS,
@@ -552,11 +559,10 @@ async def test_all_sensor(hass, cfg, regs, expected):
async def test_struct_sensor(hass, cfg, regs, expected):
"""Run test for sensor struct."""
- sensor_name = "modbus_test_sensor"
state = await base_test(
hass,
- {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, **cfg},
- sensor_name,
+ {CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, **cfg},
+ SENSOR_NAME,
SENSOR_DOMAIN,
CONF_SENSORS,
None,
@@ -568,38 +574,48 @@ async def test_struct_sensor(hass, cfg, regs, expected):
assert state == expected
-async def test_restore_state_sensor(hass):
+@pytest.mark.parametrize(
+ "mock_test_state",
+ [(State(ENTITY_ID, "117"),)],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "do_config",
+ [
+ {
+ CONF_SENSORS: [
+ {
+ CONF_NAME: SENSOR_NAME,
+ CONF_ADDRESS: 51,
+ }
+ ]
+ },
+ ],
+)
+async def test_restore_state_sensor(hass, mock_test_state, mock_modbus):
"""Run test for sensor restore state."""
-
- sensor_name = "test_sensor"
- test_value = "117"
- config_sensor = {CONF_NAME: sensor_name, CONF_ADDRESS: 17}
- mock_restore_cache(
- hass,
- (State(f"{SENSOR_DOMAIN}.{sensor_name}", test_value),),
- )
- await base_config_test(
- hass,
- config_sensor,
- sensor_name,
- SENSOR_DOMAIN,
- CONF_SENSORS,
- None,
- method_discovery=True,
- )
- entity_id = f"{SENSOR_DOMAIN}.{sensor_name}"
- assert hass.states.get(entity_id).state == test_value
+ assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state
@pytest.mark.parametrize(
- "swap_type",
- [CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE],
+ "swap_type, error_message",
+ [
+ (
+ CONF_SWAP_WORD,
+ f"Error in sensor {SENSOR_NAME} swap(word) not possible due to the registers count: 1, needed: 2",
+ ),
+ (
+ CONF_SWAP_WORD_BYTE,
+ f"Error in sensor {SENSOR_NAME} swap(word_byte) not possible due to the registers count: 1, needed: 2",
+ ),
+ ],
)
-async def test_swap_sensor_wrong_config(hass, caplog, swap_type):
+async def test_swap_sensor_wrong_config(
+ hass, caplog, swap_type, error_message, mock_pymodbus
+):
"""Run test for sensor swap."""
- sensor_name = "modbus_test_sensor"
config = {
- CONF_NAME: sensor_name,
+ CONF_NAME: SENSOR_NAME,
CONF_ADDRESS: 1234,
CONF_COUNT: 1,
CONF_SWAP: swap_type,
@@ -611,24 +627,22 @@ async def test_swap_sensor_wrong_config(hass, caplog, swap_type):
await base_config_test(
hass,
config,
- sensor_name,
+ SENSOR_NAME,
SENSOR_DOMAIN,
CONF_SENSORS,
None,
method_discovery=True,
- expect_init_to_fail=True,
+ expect_setup_to_fail=True,
)
- assert caplog.messages[-1].startswith("Error in sensor " + sensor_name + " swap")
+ assert error_message in "".join(caplog.messages)
async def test_service_sensor_update(hass, mock_pymodbus):
"""Run test for service homeassistant.update_entity."""
-
- entity_id = "sensor.test"
config = {
CONF_SENSORS: [
{
- CONF_NAME: "test",
+ CONF_NAME: SENSOR_NAME,
CONF_ADDRESS: 1234,
CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
}
@@ -640,11 +654,11 @@ async def test_service_sensor_update(hass, mock_pymodbus):
config,
)
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == "27"
+ assert hass.states.get(ENTITY_ID).state == "27"
mock_pymodbus.read_input_registers.return_value = ReadResult([32])
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == "32"
+ assert hass.states.get(ENTITY_ID).state == "32"
diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py
index 37ddfec2b4d..b31ca12c48b 100644
--- a/tests/components/modbus/test_switch.py
+++ b/tests/components/modbus/test_switch.py
@@ -39,90 +39,108 @@ from homeassistant.core import State
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
-from .conftest import ReadResult, base_config_test, base_test, prepare_service_update
+from .conftest import ReadResult, base_test, prepare_service_update
-from tests.common import async_fire_time_changed, mock_restore_cache
+from tests.common import async_fire_time_changed
+
+SWITCH_NAME = "test_switch"
+ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}"
@pytest.mark.parametrize(
"do_config",
[
{
- CONF_ADDRESS: 1234,
+ CONF_SWITCHES: [
+ {
+ CONF_NAME: SWITCH_NAME,
+ CONF_ADDRESS: 1234,
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_WRITE_TYPE: CALL_TYPE_COIL,
+ CONF_SWITCHES: [
+ {
+ CONF_NAME: SWITCH_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_WRITE_TYPE: CALL_TYPE_COIL,
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- CONF_DEVICE_CLASS: "switch",
- CONF_VERIFY: {
- CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
- CONF_ADDRESS: 1235,
- CONF_STATE_OFF: 0,
- CONF_STATE_ON: 1,
- },
+ CONF_SWITCHES: [
+ {
+ CONF_NAME: SWITCH_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_DEVICE_CLASS: "switch",
+ CONF_VERIFY: {
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_ADDRESS: 1235,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ },
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- CONF_DEVICE_CLASS: "switch",
- CONF_VERIFY: {
- CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
- CONF_ADDRESS: 1235,
- CONF_STATE_OFF: 0,
- CONF_STATE_ON: 1,
- CONF_DELAY: 10,
- },
+ CONF_SWITCHES: [
+ {
+ CONF_NAME: SWITCH_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_DEVICE_CLASS: "switch",
+ CONF_VERIFY: {
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
+ CONF_ADDRESS: 1235,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ CONF_DELAY: 10,
+ },
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- CONF_DEVICE_CLASS: "switch",
- CONF_VERIFY: {
- CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
- CONF_ADDRESS: 1235,
- CONF_STATE_OFF: 0,
- CONF_STATE_ON: 1,
- },
+ CONF_SWITCHES: [
+ {
+ CONF_NAME: SWITCH_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_DEVICE_CLASS: "switch",
+ CONF_VERIFY: {
+ CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
+ CONF_ADDRESS: 1235,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ },
+ }
+ ]
},
{
- CONF_ADDRESS: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- CONF_DEVICE_CLASS: "switch",
- CONF_SCAN_INTERVAL: 0,
- CONF_VERIFY: None,
+ CONF_SWITCHES: [
+ {
+ CONF_NAME: SWITCH_NAME,
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_DEVICE_CLASS: "switch",
+ CONF_SCAN_INTERVAL: 0,
+ CONF_VERIFY: None,
+ }
+ ]
},
],
)
-async def test_config_switch(hass, do_config):
- """Run test for switch."""
- device_name = "test_switch"
-
- device_config = {
- CONF_NAME: device_name,
- **do_config,
- }
-
- await base_config_test(
- hass,
- device_config,
- device_name,
- SWITCH_DOMAIN,
- CONF_SWITCHES,
- None,
- method_discovery=True,
- )
+async def test_config_switch(hass, mock_modbus):
+ """Run configurationtest for switch."""
+ assert SWITCH_DOMAIN in hass.config.components
@pytest.mark.parametrize("call_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING])
@@ -158,17 +176,16 @@ async def test_config_switch(hass, do_config):
)
async def test_all_switch(hass, call_type, regs, verify, expected):
"""Run test for given config."""
- switch_name = "modbus_test_switch"
state = await base_test(
hass,
{
- CONF_NAME: switch_name,
+ CONF_NAME: SWITCH_NAME,
CONF_ADDRESS: 1234,
CONF_SLAVE: 1,
CONF_WRITE_TYPE: call_type,
**verify,
},
- switch_name,
+ SWITCH_NAME,
SWITCH_DOMAIN,
CONF_SWITCHES,
None,
@@ -180,34 +197,33 @@ async def test_all_switch(hass, call_type, regs, verify, expected):
assert state == expected
-async def test_restore_state_switch(hass):
+@pytest.mark.parametrize(
+ "mock_test_state",
+ [(State(ENTITY_ID, STATE_ON),)],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "do_config",
+ [
+ {
+ CONF_SWITCHES: [
+ {
+ CONF_NAME: SWITCH_NAME,
+ CONF_ADDRESS: 1234,
+ }
+ ]
+ },
+ ],
+)
+async def test_restore_state_switch(hass, mock_test_state, mock_modbus):
"""Run test for sensor restore state."""
-
- switch_name = "test_switch"
- entity_id = f"{SWITCH_DOMAIN}.{switch_name}"
- test_value = STATE_ON
- config_switch = {CONF_NAME: switch_name, CONF_ADDRESS: 17}
- mock_restore_cache(
- hass,
- (State(f"{entity_id}", test_value),),
- )
- await base_config_test(
- hass,
- config_switch,
- switch_name,
- SWITCH_DOMAIN,
- CONF_SWITCHES,
- None,
- method_discovery=True,
- )
- assert hass.states.get(entity_id).state == test_value
+ assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state
async def test_switch_service_turn(hass, caplog, mock_pymodbus):
"""Run test for service turn_on/turn_off."""
- entity_id1 = f"{SWITCH_DOMAIN}.switch1"
- entity_id2 = f"{SWITCH_DOMAIN}.switch2"
+ ENTITY_ID2 = f"{SWITCH_DOMAIN}.{SWITCH_NAME}2"
config = {
MODBUS_DOMAIN: {
CONF_TYPE: "tcp",
@@ -215,12 +231,12 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus):
CONF_PORT: 5501,
CONF_SWITCHES: [
{
- CONF_NAME: "switch1",
+ CONF_NAME: SWITCH_NAME,
CONF_ADDRESS: 17,
CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING,
},
{
- CONF_NAME: "switch2",
+ CONF_NAME: f"{SWITCH_NAME}2",
CONF_ADDRESS: 17,
CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_VERIFY: {},
@@ -232,54 +248,53 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus):
await hass.async_block_till_done()
assert MODBUS_DOMAIN in hass.config.components
- assert hass.states.get(entity_id1).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
await hass.services.async_call(
- "switch", "turn_on", service_data={"entity_id": entity_id1}
+ "switch", "turn_on", service_data={"entity_id": ENTITY_ID}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id1).state == STATE_ON
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
await hass.services.async_call(
- "switch", "turn_off", service_data={"entity_id": entity_id1}
+ "switch", "turn_off", service_data={"entity_id": ENTITY_ID}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id1).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01])
- assert hass.states.get(entity_id2).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID2).state == STATE_OFF
await hass.services.async_call(
- "switch", "turn_on", service_data={"entity_id": entity_id2}
+ "switch", "turn_on", service_data={"entity_id": ENTITY_ID2}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id2).state == STATE_ON
+ assert hass.states.get(ENTITY_ID2).state == STATE_ON
mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00])
await hass.services.async_call(
- "switch", "turn_off", service_data={"entity_id": entity_id2}
+ "switch", "turn_off", service_data={"entity_id": ENTITY_ID2}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id2).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID2).state == STATE_OFF
mock_pymodbus.write_register.side_effect = ModbusException("fail write_")
await hass.services.async_call(
- "switch", "turn_on", service_data={"entity_id": entity_id2}
+ "switch", "turn_on", service_data={"entity_id": ENTITY_ID2}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE
+ assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE
mock_pymodbus.write_coil.side_effect = ModbusException("fail write_")
await hass.services.async_call(
- "switch", "turn_off", service_data={"entity_id": entity_id1}
+ "switch", "turn_off", service_data={"entity_id": ENTITY_ID}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id1).state == STATE_UNAVAILABLE
+ assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
async def test_service_switch_update(hass, mock_pymodbus):
"""Run test for service homeassistant.update_entity."""
- entity_id = "switch.test"
config = {
CONF_SWITCHES: [
{
- CONF_NAME: "test",
+ CONF_NAME: SWITCH_NAME,
CONF_ADDRESS: 1234,
CONF_WRITE_TYPE: CALL_TYPE_COIL,
CONF_VERIFY: {},
@@ -292,22 +307,18 @@ async def test_service_switch_update(hass, mock_pymodbus):
config,
)
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == STATE_ON
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
mock_pymodbus.read_coils.return_value = ReadResult([0x00])
await hass.services.async_call(
- "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+ "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
- assert hass.states.get(entity_id).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
async def test_delay_switch(hass, mock_pymodbus):
"""Run test for switch verify delay."""
-
- switch_name = "test_switch"
- entity_id = f"{SWITCH_DOMAIN}.{switch_name}"
-
config = {
MODBUS_DOMAIN: [
{
@@ -316,7 +327,7 @@ async def test_delay_switch(hass, mock_pymodbus):
CONF_PORT: 5501,
CONF_SWITCHES: [
{
- CONF_NAME: switch_name,
+ CONF_NAME: SWITCH_NAME,
CONF_ADDRESS: 51,
CONF_SCAN_INTERVAL: 0,
CONF_VERIFY: {
@@ -334,12 +345,12 @@ async def test_delay_switch(hass, mock_pymodbus):
assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True
await hass.async_block_till_done()
await hass.services.async_call(
- "switch", "turn_on", service_data={"entity_id": entity_id}
+ "switch", "turn_on", service_data={"entity_id": ENTITY_ID}
)
await hass.async_block_till_done()
- assert hass.states.get(entity_id).state == STATE_OFF
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
now = now + 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
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
diff --git a/tests/components/modern_forms/__init__.py b/tests/components/modern_forms/__init__.py
new file mode 100644
index 00000000000..c6d2a8b3637
--- /dev/null
+++ b/tests/components/modern_forms/__init__.py
@@ -0,0 +1,77 @@
+"""Tests for the Modern Forms integration."""
+
+import json
+from typing import Callable
+
+from aiomodernforms.const import COMMAND_QUERY_STATIC_DATA
+
+from homeassistant.components.modern_forms.const import DOMAIN
+from homeassistant.const import CONF_HOST, CONF_MAC, CONTENT_TYPE_JSON
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry, load_fixture
+from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
+
+
+async def modern_forms_call_mock(method, url, data):
+ """Set up the basic returns based on info or status request."""
+ if COMMAND_QUERY_STATIC_DATA in data:
+ fixture = "modern_forms/device_info.json"
+ else:
+ fixture = "modern_forms/device_status.json"
+ response = AiohttpClientMockResponse(
+ method=method, url=url, json=json.loads(load_fixture(fixture))
+ )
+ return response
+
+
+async def modern_forms_no_light_call_mock(method, url, data):
+ """Set up the basic returns based on info or status request."""
+ if COMMAND_QUERY_STATIC_DATA in data:
+ fixture = "modern_forms/device_info_no_light.json"
+ else:
+ fixture = "modern_forms/device_status_no_light.json"
+ response = AiohttpClientMockResponse(
+ method=method, url=url, json=json.loads(load_fixture(fixture))
+ )
+ return response
+
+
+async def modern_forms_timers_set_mock(method, url, data):
+ """Set up the basic returns based on info or status request."""
+ if COMMAND_QUERY_STATIC_DATA in data:
+ fixture = "modern_forms/device_info.json"
+ else:
+ fixture = "modern_forms/device_status_timers_active.json"
+ response = AiohttpClientMockResponse(
+ method=method, url=url, json=json.loads(load_fixture(fixture))
+ )
+ return response
+
+
+async def init_integration(
+ hass: HomeAssistant,
+ aioclient_mock: AiohttpClientMocker,
+ rgbw: bool = False,
+ skip_setup: bool = False,
+ mock_type: Callable = modern_forms_call_mock,
+) -> MockConfigEntry:
+ """Set up the Modern Forms integration in Home Assistant."""
+
+ aioclient_mock.post(
+ "http://192.168.1.123:80/mf",
+ side_effect=mock_type,
+ headers={"Content-Type": CONTENT_TYPE_JSON},
+ )
+
+ entry = MockConfigEntry(
+ domain=DOMAIN, data={CONF_HOST: "192.168.1.123", CONF_MAC: "AA:BB:CC:DD:EE:FF"}
+ )
+
+ entry.add_to_hass(hass)
+
+ if not skip_setup:
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ return entry
diff --git a/tests/components/modern_forms/test_binary_sensor.py b/tests/components/modern_forms/test_binary_sensor.py
new file mode 100644
index 00000000000..bc32e309958
--- /dev/null
+++ b/tests/components/modern_forms/test_binary_sensor.py
@@ -0,0 +1,46 @@
+"""Tests for the Modern Forms sensor platform."""
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.components.modern_forms.const import DOMAIN
+from homeassistant.const import ATTR_ICON
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from tests.components.modern_forms import init_integration
+from tests.test_util.aiohttp import AiohttpClientMocker
+
+
+async def test_binary_sensors(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the creation and values of the Modern Forms sensors."""
+
+ registry = er.async_get(hass)
+
+ registry.async_get_or_create(
+ BINARY_SENSOR_DOMAIN,
+ DOMAIN,
+ "AA:BB:CC:DD:EE:FF_light_sleep_timer_active",
+ suggested_object_id="modernformsfan_light_sleep_timer_active",
+ disabled_by=None,
+ )
+ registry.async_get_or_create(
+ BINARY_SENSOR_DOMAIN,
+ DOMAIN,
+ "AA:BB:CC:DD:EE:FF_fan_sleep_timer_active",
+ suggested_object_id="modernformsfan_fan_sleep_timer_active",
+ disabled_by=None,
+ )
+
+ await init_integration(hass, aioclient_mock)
+
+ # Light timer remaining time
+ state = hass.states.get("binary_sensor.modernformsfan_light_sleep_timer_active")
+ assert state
+ assert state.attributes.get(ATTR_ICON) == "mdi:av-timer"
+ assert state.state == "off"
+
+ # Fan timer remaining time
+ state = hass.states.get("binary_sensor.modernformsfan_fan_sleep_timer_active")
+ assert state
+ assert state.attributes.get(ATTR_ICON) == "mdi:av-timer"
+ assert state.state == "off"
diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py
new file mode 100644
index 00000000000..967e9d354d5
--- /dev/null
+++ b/tests/components/modern_forms/test_config_flow.py
@@ -0,0 +1,227 @@
+"""Tests for the Modern Forms config flow."""
+from unittest.mock import MagicMock, patch
+
+import aiohttp
+from aiomodernforms import ModernFormsConnectionError
+
+from homeassistant.components.modern_forms.const import DOMAIN
+from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
+from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON
+from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import (
+ RESULT_TYPE_ABORT,
+ RESULT_TYPE_CREATE_ENTRY,
+ RESULT_TYPE_FORM,
+)
+
+from . import init_integration
+
+from tests.common import load_fixture
+from tests.test_util.aiohttp import AiohttpClientMocker
+
+
+async def test_full_user_flow_implementation(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the full manual user flow from start to finish."""
+ aioclient_mock.post(
+ "http://192.168.1.123:80/mf",
+ text=load_fixture("modern_forms/device_info.json"),
+ headers={"Content-Type": CONTENT_TYPE_JSON},
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ )
+
+ assert result.get("step_id") == "user"
+ assert result.get("type") == RESULT_TYPE_FORM
+ assert "flow_id" in result
+
+ with patch(
+ "homeassistant.components.modern_forms.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={CONF_HOST: "192.168.1.123"}
+ )
+
+ assert result2.get("title") == "ModernFormsFan"
+ assert "data" in result2
+ assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
+ assert result2["data"][CONF_HOST] == "192.168.1.123"
+ assert result2["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF"
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_full_zeroconf_flow_implementation(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the full manual user flow from start to finish."""
+ aioclient_mock.post(
+ "http://192.168.1.123:80/mf",
+ text=load_fixture("modern_forms/device_info.json"),
+ headers={"Content-Type": CONTENT_TYPE_JSON},
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_ZEROCONF},
+ data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}},
+ )
+
+ flows = hass.config_entries.flow.async_progress()
+ assert len(flows) == 1
+
+ assert result.get("description_placeholders") == {CONF_NAME: "example"}
+ assert result.get("step_id") == "zeroconf_confirm"
+ assert result.get("type") == RESULT_TYPE_FORM
+ assert "flow_id" in result
+
+ flow = flows[0]
+ assert "context" in flow
+ assert flow["context"][CONF_HOST] == "192.168.1.123"
+ assert flow["context"][CONF_NAME] == "example"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={}
+ )
+
+ assert result2.get("title") == "example"
+ assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
+
+ assert "data" in result2
+ assert result2["data"][CONF_HOST] == "192.168.1.123"
+ assert result2["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF"
+
+
+@patch(
+ "homeassistant.components.modern_forms.ModernFormsDevice.update",
+ side_effect=ModernFormsConnectionError,
+)
+async def test_connection_error(
+ update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test we show user form on Modern Forms connection error."""
+ aioclient_mock.post("http://example.com/mf", exc=aiohttp.ClientError)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={CONF_HOST: "example.com"},
+ )
+
+ assert result.get("type") == RESULT_TYPE_FORM
+ assert result.get("step_id") == "user"
+ assert result.get("errors") == {"base": "cannot_connect"}
+
+
+@patch(
+ "homeassistant.components.modern_forms.ModernFormsDevice.update",
+ side_effect=ModernFormsConnectionError,
+)
+async def test_zeroconf_connection_error(
+ update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test we abort zeroconf flow on Modern Forms connection error."""
+ aioclient_mock.post("http://192.168.1.123/mf", exc=aiohttp.ClientError)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_ZEROCONF},
+ data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}},
+ )
+
+ assert result.get("type") == RESULT_TYPE_ABORT
+ assert result.get("reason") == "cannot_connect"
+
+
+@patch(
+ "homeassistant.components.modern_forms.ModernFormsDevice.update",
+ side_effect=ModernFormsConnectionError,
+)
+async def test_zeroconf_confirm_connection_error(
+ update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test we abort zeroconf flow on Modern Forms connection error."""
+ aioclient_mock.post("http://192.168.1.123:80/mf", exc=aiohttp.ClientError)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": SOURCE_ZEROCONF,
+ CONF_HOST: "example.com",
+ CONF_NAME: "test",
+ },
+ data={"host": "192.168.1.123", "hostname": "example.com.", "properties": {}},
+ )
+
+ assert result.get("type") == RESULT_TYPE_ABORT
+ assert result.get("reason") == "cannot_connect"
+
+
+async def test_user_device_exists_abort(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test we abort zeroconf flow if Modern Forms device already configured."""
+ aioclient_mock.post(
+ "http://192.168.1.123:80/mf",
+ text=load_fixture("modern_forms/device_info.json"),
+ headers={"Content-Type": CONTENT_TYPE_JSON},
+ )
+
+ await init_integration(hass, aioclient_mock, skip_setup=True)
+
+ await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={
+ "host": "192.168.1.123",
+ "hostname": "example.local.",
+ "properties": {CONF_MAC: "AA:BB:CC:DD:EE:FF"},
+ },
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={
+ "host": "192.168.1.123",
+ "hostname": "example.local.",
+ "properties": {CONF_MAC: "AA:BB:CC:DD:EE:FF"},
+ },
+ )
+
+ assert result.get("type") == RESULT_TYPE_ABORT
+ assert result.get("reason") == "already_configured"
+
+
+async def test_zeroconf_with_mac_device_exists_abort(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test we abort zeroconf flow if a Modern Forms device already configured."""
+ await init_integration(hass, aioclient_mock, skip_setup=True)
+
+ await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={
+ "host": "192.168.1.123",
+ "hostname": "example.local.",
+ "properties": {CONF_MAC: "AA:BB:CC:DD:EE:FF"},
+ },
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_ZEROCONF},
+ data={
+ "host": "192.168.1.123",
+ "hostname": "example.local.",
+ "properties": {CONF_MAC: "AA:BB:CC:DD:EE:FF"},
+ },
+ )
+
+ assert result.get("type") == RESULT_TYPE_ABORT
+ assert result.get("reason") == "already_configured"
diff --git a/tests/components/modern_forms/test_fan.py b/tests/components/modern_forms/test_fan.py
new file mode 100644
index 00000000000..c9b6c66bb62
--- /dev/null
+++ b/tests/components/modern_forms/test_fan.py
@@ -0,0 +1,213 @@
+"""Tests for the Modern Forms fan platform."""
+from unittest.mock import patch
+
+from aiomodernforms import ModernFormsConnectionError
+
+from homeassistant.components.fan import (
+ ATTR_DIRECTION,
+ ATTR_PERCENTAGE,
+ DIRECTION_FORWARD,
+ DIRECTION_REVERSE,
+ DOMAIN as FAN_DOMAIN,
+ SERVICE_SET_DIRECTION,
+ SERVICE_SET_PERCENTAGE,
+)
+from homeassistant.components.modern_forms.const import (
+ ATTR_SLEEP_TIME,
+ DOMAIN,
+ SERVICE_CLEAR_FAN_SLEEP_TIMER,
+ SERVICE_SET_FAN_SLEEP_TIMER,
+)
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_ON,
+ STATE_UNAVAILABLE,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from tests.components.modern_forms import init_integration
+from tests.test_util.aiohttp import AiohttpClientMocker
+
+
+async def test_fan_state(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the creation and values of the Modern Forms fans."""
+ await init_integration(hass, aioclient_mock)
+
+ entity_registry = er.async_get(hass)
+
+ state = hass.states.get("fan.modernformsfan_fan")
+ assert state
+ assert state.attributes.get(ATTR_PERCENTAGE) == 50
+ assert state.attributes.get(ATTR_DIRECTION) == DIRECTION_FORWARD
+ assert state.state == STATE_ON
+
+ entry = entity_registry.async_get("fan.modernformsfan_fan")
+ assert entry
+ assert entry.unique_id == "AA:BB:CC:DD:EE:FF"
+
+
+async def test_change_state(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+) -> None:
+ """Test the change of state of the Modern Forms fan."""
+ await init_integration(hass, aioclient_mock)
+
+ with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "fan.modernformsfan_fan"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ fan_mock.assert_called_once_with(
+ on=False,
+ )
+
+ with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: "fan.modernformsfan_fan",
+ ATTR_PERCENTAGE: 100,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ fan_mock.assert_called_once_with(on=True, speed=6)
+
+
+async def test_sleep_timer_services(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+) -> None:
+ """Test the change of state of the Modern Forms segments."""
+ await init_integration(hass, aioclient_mock)
+
+ with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_FAN_SLEEP_TIMER,
+ {ATTR_ENTITY_ID: "fan.modernformsfan_fan", ATTR_SLEEP_TIME: 1},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ fan_mock.assert_called_once_with(sleep=60)
+
+ with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_CLEAR_FAN_SLEEP_TIMER,
+ {ATTR_ENTITY_ID: "fan.modernformsfan_fan"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ fan_mock.assert_called_once_with(sleep=0)
+
+
+async def test_change_direction(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+) -> None:
+ """Test the change of state of the Modern Forms segments."""
+ await init_integration(hass, aioclient_mock)
+
+ with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_DIRECTION,
+ {
+ ATTR_ENTITY_ID: "fan.modernformsfan_fan",
+ ATTR_DIRECTION: DIRECTION_REVERSE,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ fan_mock.assert_called_once_with(
+ direction=DIRECTION_REVERSE,
+ )
+
+
+async def test_set_percentage(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+) -> None:
+ """Test the change of percentage for the Modern Forms fan."""
+ await init_integration(hass, aioclient_mock)
+ with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_PERCENTAGE,
+ {
+ ATTR_ENTITY_ID: "fan.modernformsfan_fan",
+ ATTR_PERCENTAGE: 100,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ fan_mock.assert_called_once_with(
+ on=True,
+ speed=6,
+ )
+
+ await init_integration(hass, aioclient_mock)
+ with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_PERCENTAGE,
+ {
+ ATTR_ENTITY_ID: "fan.modernformsfan_fan",
+ ATTR_PERCENTAGE: 0,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ fan_mock.assert_called_once_with(on=False)
+
+
+async def test_fan_error(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+) -> None:
+ """Test error handling of the Modern Forms fans."""
+
+ await init_integration(hass, aioclient_mock)
+ aioclient_mock.clear_requests()
+
+ aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=400)
+
+ with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"):
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "fan.modernformsfan_fan"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ state = hass.states.get("fan.modernformsfan_fan")
+ assert state.state == STATE_ON
+ assert "Invalid response from API" in caplog.text
+
+
+async def test_fan_connection_error(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test error handling of the Moder Forms fans."""
+ await init_integration(hass, aioclient_mock)
+
+ with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), patch(
+ "homeassistant.components.modern_forms.ModernFormsDevice.fan",
+ side_effect=ModernFormsConnectionError,
+ ):
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "fan.modernformsfan_fan"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("fan.modernformsfan_fan")
+ assert state.state == STATE_UNAVAILABLE
diff --git a/tests/components/modern_forms/test_init.py b/tests/components/modern_forms/test_init.py
new file mode 100644
index 00000000000..518355ac18b
--- /dev/null
+++ b/tests/components/modern_forms/test_init.py
@@ -0,0 +1,52 @@
+"""Tests for the Modern Forms integration."""
+from unittest.mock import MagicMock, patch
+
+from aiomodernforms import ModernFormsConnectionError
+
+from homeassistant.components.modern_forms.const import DOMAIN
+from homeassistant.config_entries import ConfigEntryState
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from tests.components.modern_forms import (
+ init_integration,
+ modern_forms_no_light_call_mock,
+)
+from tests.test_util.aiohttp import AiohttpClientMocker
+
+
+@patch(
+ "homeassistant.components.modern_forms.ModernFormsDevice.update",
+ side_effect=ModernFormsConnectionError,
+)
+async def test_config_entry_not_ready(
+ mock_update: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the Modern Forms configuration entry not ready."""
+ entry = await init_integration(hass, aioclient_mock)
+ assert entry.state is ConfigEntryState.SETUP_RETRY
+
+
+async def test_unload_config_entry(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the Modern Forms configuration entry unloading."""
+ entry = await init_integration(hass, aioclient_mock)
+ assert hass.data[DOMAIN]
+
+ await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+ assert not hass.data.get(DOMAIN)
+
+
+async def test_fan_only_device(hass, aioclient_mock):
+ """Test we set unique ID if not set yet."""
+ await init_integration(
+ hass, aioclient_mock, mock_type=modern_forms_no_light_call_mock
+ )
+ entity_registry = er.async_get(hass)
+
+ fan_entry = entity_registry.async_get("fan.modernformsfan_fan")
+ assert fan_entry
+ light_entry = entity_registry.async_get("light.modernformsfan_light")
+ assert light_entry is None
diff --git a/tests/components/modern_forms/test_light.py b/tests/components/modern_forms/test_light.py
new file mode 100644
index 00000000000..29725ab4bcd
--- /dev/null
+++ b/tests/components/modern_forms/test_light.py
@@ -0,0 +1,145 @@
+"""Tests for the Modern Forms light platform."""
+from unittest.mock import patch
+
+from aiomodernforms import ModernFormsConnectionError
+
+from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN
+from homeassistant.components.modern_forms.const import (
+ ATTR_SLEEP_TIME,
+ DOMAIN,
+ SERVICE_CLEAR_LIGHT_SLEEP_TIMER,
+ SERVICE_SET_LIGHT_SLEEP_TIMER,
+)
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_FRIENDLY_NAME,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_ON,
+ STATE_UNAVAILABLE,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from tests.components.modern_forms import init_integration
+from tests.test_util.aiohttp import AiohttpClientMocker
+
+
+async def test_light_state(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the creation and values of the Modern Forms lights."""
+ await init_integration(hass, aioclient_mock)
+
+ entity_registry = er.async_get(hass)
+
+ state = hass.states.get("light.modernformsfan_light")
+ assert state
+ assert state.attributes.get(ATTR_BRIGHTNESS) == 128
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "ModernFormsFan Light"
+ assert state.state == STATE_ON
+
+ entry = entity_registry.async_get("light.modernformsfan_light")
+ assert entry
+ assert entry.unique_id == "AA:BB:CC:DD:EE:FF"
+
+
+async def test_change_state(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+) -> None:
+ """Test the change of state of the Modern Forms segments."""
+ await init_integration(hass, aioclient_mock)
+
+ with patch("aiomodernforms.ModernFormsDevice.light") as light_mock:
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.modernformsfan_light"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ light_mock.assert_called_once_with(
+ on=False,
+ )
+
+ with patch("aiomodernforms.ModernFormsDevice.light") as light_mock:
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "light.modernformsfan_light", ATTR_BRIGHTNESS: 255},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ light_mock.assert_called_once_with(on=True, brightness=100)
+
+
+async def test_sleep_timer_services(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+) -> None:
+ """Test the change of state of the Modern Forms segments."""
+ await init_integration(hass, aioclient_mock)
+
+ with patch("aiomodernforms.ModernFormsDevice.light") as light_mock:
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_LIGHT_SLEEP_TIMER,
+ {ATTR_ENTITY_ID: "light.modernformsfan_light", ATTR_SLEEP_TIME: 1},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ light_mock.assert_called_once_with(sleep=60)
+
+ with patch("aiomodernforms.ModernFormsDevice.light") as light_mock:
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_CLEAR_LIGHT_SLEEP_TIMER,
+ {ATTR_ENTITY_ID: "light.modernformsfan_light"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ light_mock.assert_called_once_with(sleep=0)
+
+
+async def test_light_error(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+) -> None:
+ """Test error handling of the Modern Forms lights."""
+
+ await init_integration(hass, aioclient_mock)
+ aioclient_mock.clear_requests()
+
+ aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=400)
+
+ with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"):
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.modernformsfan_light"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ state = hass.states.get("light.modernformsfan_light")
+ assert state.state == STATE_ON
+ assert "Invalid response from API" in caplog.text
+
+
+async def test_light_connection_error(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test error handling of the Moder Forms lights."""
+ await init_integration(hass, aioclient_mock)
+
+ with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), patch(
+ "homeassistant.components.modern_forms.ModernFormsDevice.light",
+ side_effect=ModernFormsConnectionError,
+ ):
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.modernformsfan_light"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.modernformsfan_light")
+ assert state.state == STATE_UNAVAILABLE
diff --git a/tests/components/modern_forms/test_sensor.py b/tests/components/modern_forms/test_sensor.py
new file mode 100644
index 00000000000..d18793f51c2
--- /dev/null
+++ b/tests/components/modern_forms/test_sensor.py
@@ -0,0 +1,57 @@
+"""Tests for the Modern Forms sensor platform."""
+from datetime import datetime
+
+from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, DEVICE_CLASS_TIMESTAMP
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from tests.components.modern_forms import init_integration, modern_forms_timers_set_mock
+from tests.test_util.aiohttp import AiohttpClientMocker
+
+
+async def test_sensors(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the creation and values of the Modern Forms sensors."""
+
+ # await init_integration(hass, aioclient_mock)
+ await init_integration(hass, aioclient_mock)
+ er.async_get(hass)
+
+ # Light timer remaining time
+ state = hass.states.get("sensor.modernformsfan_light_sleep_time")
+ assert state
+ assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline"
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP
+ assert state.state == "unknown"
+
+ # Fan timer remaining time
+ state = hass.states.get("sensor.modernformsfan_fan_sleep_time")
+ assert state
+ assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline"
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP
+ assert state.state == "unknown"
+
+
+async def test_active_sensors(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the creation and values of the Modern Forms sensors."""
+
+ # await init_integration(hass, aioclient_mock)
+ await init_integration(hass, aioclient_mock, mock_type=modern_forms_timers_set_mock)
+ er.async_get(hass)
+
+ # Light timer remaining time
+ state = hass.states.get("sensor.modernformsfan_light_sleep_time")
+ assert state
+ assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline"
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP
+ datetime.fromisoformat(state.state)
+
+ # Fan timer remaining time
+ state = hass.states.get("sensor.modernformsfan_fan_sleep_time")
+ assert state
+ assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline"
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP
+ datetime.fromisoformat(state.state)
diff --git a/tests/components/modern_forms/test_switch.py b/tests/components/modern_forms/test_switch.py
new file mode 100644
index 00000000000..963264e3ca1
--- /dev/null
+++ b/tests/components/modern_forms/test_switch.py
@@ -0,0 +1,144 @@
+"""Tests for the Modern Forms switch platform."""
+from unittest.mock import patch
+
+from aiomodernforms import ModernFormsConnectionError
+
+from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_ICON,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_OFF,
+ STATE_UNAVAILABLE,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from tests.components.modern_forms import init_integration
+from tests.test_util.aiohttp import AiohttpClientMocker
+
+
+async def test_switch_state(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the creation and values of the Modern Forms switches."""
+ await init_integration(hass, aioclient_mock)
+
+ entity_registry = er.async_get(hass)
+
+ state = hass.states.get("switch.modernformsfan_away_mode")
+ assert state
+ assert state.attributes.get(ATTR_ICON) == "mdi:airplane-takeoff"
+ assert state.state == STATE_OFF
+
+ entry = entity_registry.async_get("switch.modernformsfan_away_mode")
+ assert entry
+ assert entry.unique_id == "AA:BB:CC:DD:EE:FF_away_mode"
+
+ state = hass.states.get("switch.modernformsfan_adaptive_learning")
+ assert state
+ assert state.attributes.get(ATTR_ICON) == "mdi:school-outline"
+ assert state.state == STATE_OFF
+
+ entry = entity_registry.async_get("switch.modernformsfan_adaptive_learning")
+ assert entry
+ assert entry.unique_id == "AA:BB:CC:DD:EE:FF_adaptive_learning"
+
+
+async def test_switch_change_state(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the change of state of the Modern Forms switches."""
+ await init_integration(hass, aioclient_mock)
+
+ # Away Mode
+ with patch("aiomodernforms.ModernFormsDevice.away") as away_mock:
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ away_mock.assert_called_once_with(away=True)
+
+ with patch("aiomodernforms.ModernFormsDevice.away") as away_mock:
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ away_mock.assert_called_once_with(away=False)
+
+ # Adaptive Learning
+ with patch(
+ "aiomodernforms.ModernFormsDevice.adaptive_learning"
+ ) as adaptive_learning_mock:
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.modernformsfan_adaptive_learning"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ adaptive_learning_mock.assert_called_once_with(adaptive_learning=True)
+
+ with patch(
+ "aiomodernforms.ModernFormsDevice.adaptive_learning"
+ ) as adaptive_learning_mock:
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.modernformsfan_adaptive_learning"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ adaptive_learning_mock.assert_called_once_with(adaptive_learning=False)
+
+
+async def test_switch_error(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+) -> None:
+ """Test error handling of the Modern Forms switches."""
+ await init_integration(hass, aioclient_mock)
+
+ aioclient_mock.clear_requests()
+ aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=400)
+
+ with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"):
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("switch.modernformsfan_away_mode")
+ assert state.state == STATE_OFF
+ assert "Invalid response from API" in caplog.text
+
+
+async def test_switch_connection_error(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test error handling of the Modern Forms switches."""
+ await init_integration(hass, aioclient_mock)
+
+ with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), patch(
+ "homeassistant.components.modern_forms.ModernFormsDevice.away",
+ side_effect=ModernFormsConnectionError,
+ ):
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("switch.modernformsfan_away_mode")
+ assert state.state == STATE_UNAVAILABLE
diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py
index cfb59ba27a7..c9d06ef343e 100644
--- a/tests/components/mqtt/test_alarm_control_panel.py
+++ b/tests/components/mqtt/test_alarm_control_panel.py
@@ -6,6 +6,9 @@ from unittest.mock import patch
import pytest
from homeassistant.components import alarm_control_panel
+from homeassistant.components.mqtt.alarm_control_panel import (
+ MQTT_ALARM_ATTRIBUTES_BLOCKED,
+)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
@@ -39,6 +42,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -544,6 +548,17 @@ 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,
+ alarm_control_panel.DOMAIN,
+ DEFAULT_CONFIG,
+ MQTT_ALARM_ATTRIBUTES_BLOCKED,
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py
index 13c15796cfd..a73a63d3439 100644
--- a/tests/components/mqtt/test_camera.py
+++ b/tests/components/mqtt/test_camera.py
@@ -5,6 +5,7 @@ from unittest.mock import patch
import pytest
from homeassistant.components import camera
+from homeassistant.components.mqtt.camera import MQTT_CAMERA_ATTRIBUTES_BLOCKED
from homeassistant.setup import async_setup_component
from .test_common import (
@@ -26,6 +27,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -94,6 +96,13 @@ 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, camera.DOMAIN, DEFAULT_CONFIG, MQTT_CAMERA_ATTRIBUTES_BLOCKED
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py
index 546c112b153..24c9e4a5b74 100644
--- a/tests/components/mqtt/test_climate.py
+++ b/tests/components/mqtt/test_climate.py
@@ -23,6 +23,7 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
+from homeassistant.components.mqtt.climate import MQTT_CLIMATE_ATTRIBUTES_BLOCKED
from homeassistant.const import STATE_OFF
from homeassistant.setup import async_setup_component
@@ -45,6 +46,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -923,6 +925,13 @@ 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, CLIMATE_DOMAIN, DEFAULT_CONFIG, MQTT_CLIMATE_ATTRIBUTES_BLOCKED
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py
index fc8d26843d0..c3a4022fdd8 100644
--- a/tests/components/mqtt/test_common.py
+++ b/tests/components/mqtt/test_common.py
@@ -7,6 +7,7 @@ from unittest.mock import ANY, patch
from homeassistant.components import mqtt
from homeassistant.components.mqtt import debug_info
from homeassistant.components.mqtt.const import MQTT_DISCONNECTED
+from homeassistant.components.mqtt.mixins import MQTT_ATTRIBUTES_BLOCKED
from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -493,6 +494,34 @@ async def help_test_setting_attribute_via_mqtt_json_message(
assert state.attributes.get("val") == "100"
+async def help_test_setting_blocked_attribute_via_mqtt_json_message(
+ hass, mqtt_mock, domain, config, extra_blocked_attributes
+):
+ """Test the setting of blocked attribute via MQTT with JSON payload.
+
+ This is a test helper for the MqttAttributes mixin.
+ """
+ extra_blocked_attributes = extra_blocked_attributes or []
+
+ # Add JSON attributes settings to config
+ config = copy.deepcopy(config)
+ config[domain]["json_attributes_topic"] = "attr-topic"
+ data = json.dumps(config[domain])
+ async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data)
+ await hass.async_block_till_done()
+ val = "abc123"
+
+ for attr in MQTT_ATTRIBUTES_BLOCKED:
+ async_fire_mqtt_message(hass, "attr-topic", json.dumps({attr: val}))
+ state = hass.states.get(f"{domain}.test")
+ assert state.attributes.get(attr) != val
+
+ for attr in extra_blocked_attributes:
+ async_fire_mqtt_message(hass, "attr-topic", json.dumps({attr: val}))
+ state = hass.states.get(f"{domain}.test")
+ assert state.attributes.get(attr) != val
+
+
async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, config):
"""Test the setting of attribute via MQTT with JSON payload.
diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py
index c5a8552dc20..0ee2557fbd6 100644
--- a/tests/components/mqtt/test_cover.py
+++ b/tests/components/mqtt/test_cover.py
@@ -20,6 +20,7 @@ from homeassistant.components.mqtt.cover import (
CONF_TILT_COMMAND_TOPIC,
CONF_TILT_STATUS_TEMPLATE,
CONF_TILT_STATUS_TOPIC,
+ MQTT_COVER_ATTRIBUTES_BLOCKED,
MqttCover,
)
from homeassistant.const import (
@@ -62,6 +63,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -391,6 +393,34 @@ async def test_position_via_template_and_entity_id(hass, mqtt_mock):
assert current_cover_position == 20
+@pytest.mark.parametrize(
+ "config, assumed_state",
+ [
+ ({"command_topic": "abc"}, True),
+ ({"command_topic": "abc", "state_topic": "abc"}, False),
+ # ({"set_position_topic": "abc"}, True), - not a valid configuration
+ ({"set_position_topic": "abc", "position_topic": "abc"}, False),
+ ({"tilt_command_topic": "abc"}, True),
+ ({"tilt_command_topic": "abc", "tilt_status_topic": "abc"}, False),
+ ],
+)
+async def test_optimistic_flag(hass, mqtt_mock, config, assumed_state):
+ """Test assumed_state is set correctly."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {cover.DOMAIN: {**config, "platform": "mqtt", "name": "test", "qos": 0}},
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_UNKNOWN
+ if assumed_state:
+ assert ATTR_ASSUMED_STATE in state.attributes
+ else:
+ assert ATTR_ASSUMED_STATE not in state.attributes
+
+
async def test_optimistic_state_change(hass, mqtt_mock):
"""Test changing state optimistically."""
assert await async_setup_component(
@@ -2307,6 +2337,13 @@ 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, cover.DOMAIN, DEFAULT_CONFIG, MQTT_COVER_ATTRIBUTES_BLOCKED
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py
index 174db8f017a..4020c2beaeb 100644
--- a/tests/components/mqtt/test_device_tracker_discovery.py
+++ b/tests/components/mqtt/test_device_tracker_discovery.py
@@ -2,11 +2,22 @@
import pytest
+from homeassistant.components import device_tracker
from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED
from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN
+from .test_common import help_test_setting_blocked_attribute_via_mqtt_json_message
+
from tests.common import async_fire_mqtt_message, mock_device_registry, mock_registry
+DEFAULT_CONFIG = {
+ device_tracker.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "test-topic",
+ }
+}
+
@pytest.fixture
def device_reg(hass):
@@ -360,3 +371,10 @@ async def test_setting_device_tracker_location_via_lat_lon_message(
state = hass.states.get("device_tracker.test")
assert state.attributes["latitude"] == 32.87336
assert state.state == STATE_UNKNOWN
+
+
+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, device_tracker.DOMAIN, DEFAULT_CONFIG, None
+ )
diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py
index d55c8e0eccc..907c3d398b6 100644
--- a/tests/components/mqtt/test_discovery.py
+++ b/tests/components/mqtt/test_discovery.py
@@ -55,18 +55,30 @@ async def test_subscribing_config_topic(hass, mqtt_mock):
assert discovery_topic + "/+/+/+/config" in topics
-async def test_invalid_topic(hass, mqtt_mock):
+@pytest.mark.parametrize(
+ "topic, log",
+ [
+ ("homeassistant/binary_sensor/bla/not_config", False),
+ ("homeassistant/binary_sensor/rörkrökare/config", True),
+ ],
+)
+async def test_invalid_topic(hass, mqtt_mock, caplog, topic, log):
"""Test sending to invalid topic."""
with patch(
"homeassistant.components.mqtt.discovery.async_dispatcher_send"
) as mock_dispatcher_send:
mock_dispatcher_send = AsyncMock(return_value=None)
- async_fire_mqtt_message(
- hass, "homeassistant/binary_sensor/bla/not_config", "{}"
- )
+ async_fire_mqtt_message(hass, topic, "{}")
await hass.async_block_till_done()
assert not mock_dispatcher_send.called
+ if log:
+ assert (
+ f"Received message on illegal discovery topic '{topic}'" in caplog.text
+ )
+ else:
+ assert "Received message on illegal discovery topic'" not in caplog.text
+ caplog.clear()
async def test_invalid_json(hass, mqtt_mock, caplog):
diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py
index dad365e3c66..438ae0978c6 100644
--- a/tests/components/mqtt/test_fan.py
+++ b/tests/components/mqtt/test_fan.py
@@ -6,6 +6,7 @@ from voluptuous.error import MultipleInvalid
from homeassistant.components import fan
from homeassistant.components.fan import NotValidPresetModeError
+from homeassistant.components.mqtt.fan import MQTT_FAN_ATTRIBUTES_BLOCKED
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_SUPPORTED_FEATURES,
@@ -33,6 +34,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -2037,6 +2039,13 @@ 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, fan.DOMAIN, DEFAULT_CONFIG, MQTT_FAN_ATTRIBUTES_BLOCKED
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py
index db25e66c2c2..2dc8993a565 100644
--- a/tests/components/mqtt/test_legacy_vacuum.py
+++ b/tests/components/mqtt/test_legacy_vacuum.py
@@ -11,6 +11,7 @@ from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum
from homeassistant.components.mqtt.vacuum.schema import services_to_strings
from homeassistant.components.mqtt.vacuum.schema_legacy import (
ALL_SERVICES,
+ MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED,
SERVICE_TO_STRING,
)
from homeassistant.components.vacuum import (
@@ -42,6 +43,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -581,6 +583,17 @@ 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,
+ vacuum.DOMAIN,
+ DEFAULT_CONFIG_2,
+ MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED,
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py
index e419743bd87..7341eeb67fc 100644
--- a/tests/components/mqtt/test_light.py
+++ b/tests/components/mqtt/test_light.py
@@ -161,6 +161,9 @@ import pytest
from homeassistant import config as hass_config
from homeassistant.components import light
+from homeassistant.components.mqtt.light.schema_basic import (
+ MQTT_LIGHT_ATTRIBUTES_BLOCKED,
+)
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_SUPPORTED_FEATURES,
@@ -190,6 +193,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -1103,7 +1107,7 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock):
assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
-async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock):
+async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock, caplog):
"""Test the setting of the state with undocumented value_template."""
config = {
light.DOMAIN: {
@@ -1118,6 +1122,8 @@ async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock):
assert await async_setup_component(hass, light.DOMAIN, config)
await hass.async_block_till_done()
+ assert "The 'value_template' option is deprecated" in caplog.text
+
state = hass.states.get("light.test")
assert state.state == STATE_OFF
@@ -2266,6 +2272,83 @@ async def test_on_command_rgbww_template(hass, mqtt_mock):
mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False)
+async def test_on_command_white(hass, mqtt_mock):
+ """Test sending commands for RGB + white light."""
+ config = {
+ light.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "tasmota_B94927/cmnd/POWER",
+ "value_template": "{{ value_json.POWER }}",
+ "payload_off": "OFF",
+ "payload_on": "ON",
+ "brightness_command_topic": "tasmota_B94927/cmnd/Dimmer",
+ "brightness_scale": 100,
+ "on_command_type": "brightness",
+ "brightness_value_template": "{{ value_json.Dimmer }}",
+ "rgb_command_topic": "tasmota_B94927/cmnd/Color2",
+ "rgb_value_template": "{{value_json.Color.split(',')[0:3]|join(',')}}",
+ "white_command_topic": "tasmota_B94927/cmnd/White",
+ "white_scale": 100,
+ "color_mode_value_template": "{% if value_json.White %} white {% else %} rgb {% endif %}",
+ "qos": "0",
+ }
+ }
+ color_modes = ["rgb", "white"]
+
+ assert await async_setup_component(hass, light.DOMAIN, config)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get("brightness") is None
+ assert state.attributes.get("rgb_color") is None
+ assert state.attributes.get(light.ATTR_COLOR_MODE) is None
+ assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_on(hass, "light.test", brightness=192)
+ mqtt_mock.async_publish.assert_has_calls(
+ [
+ call("tasmota_B94927/cmnd/Dimmer", "75", 0, False),
+ ],
+ any_order=True,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ await common.async_turn_on(hass, "light.test", white=255)
+ mqtt_mock.async_publish.assert_has_calls(
+ [
+ call("tasmota_B94927/cmnd/White", "100", 0, False),
+ ],
+ any_order=True,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ await common.async_turn_on(hass, "light.test", white=64)
+ mqtt_mock.async_publish.assert_has_calls(
+ [
+ call("tasmota_B94927/cmnd/White", "25", 0, False),
+ ],
+ any_order=True,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ await common.async_turn_on(hass, "light.test")
+ mqtt_mock.async_publish.assert_has_calls(
+ [
+ call("tasmota_B94927/cmnd/Dimmer", "25", 0, False),
+ ],
+ any_order=True,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ await common.async_turn_off(hass, "light.test")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_B94927/cmnd/POWER", "OFF", 0, False
+ )
+
+
async def test_explicit_color_mode(hass, mqtt_mock):
"""Test explicit color mode over mqtt."""
config = {
@@ -2497,6 +2580,70 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock):
assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
+async def test_white_state_update(hass, mqtt_mock):
+ """Test state updates for RGB + white light."""
+ config = {
+ light.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "tasmota_B94927/tele/STATE",
+ "command_topic": "tasmota_B94927/cmnd/POWER",
+ "value_template": "{{ value_json.POWER }}",
+ "payload_off": "OFF",
+ "payload_on": "ON",
+ "brightness_command_topic": "tasmota_B94927/cmnd/Dimmer",
+ "brightness_state_topic": "tasmota_B94927/tele/STATE",
+ "brightness_scale": 100,
+ "on_command_type": "brightness",
+ "brightness_value_template": "{{ value_json.Dimmer }}",
+ "rgb_command_topic": "tasmota_B94927/cmnd/Color2",
+ "rgb_state_topic": "tasmota_B94927/tele/STATE",
+ "rgb_value_template": "{{value_json.Color.split(',')[0:3]|join(',')}}",
+ "white_command_topic": "tasmota_B94927/cmnd/White",
+ "white_scale": 100,
+ "color_mode_state_topic": "tasmota_B94927/tele/STATE",
+ "color_mode_value_template": "{% if value_json.White %} white {% else %} rgb {% endif %}",
+ "qos": "0",
+ }
+ }
+ color_modes = ["rgb", "white"]
+
+ assert await async_setup_component(hass, light.DOMAIN, config)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get("brightness") is None
+ assert state.attributes.get("rgb_color") is None
+ assert state.attributes.get(light.ATTR_COLOR_MODE) is None
+ assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_B94927/tele/STATE",
+ '{"POWER":"ON","Dimmer":50,"Color":"0,0,0,128","White":50}',
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("brightness") == 128
+ assert state.attributes.get("rgb_color") is None
+ assert state.attributes.get(light.ATTR_COLOR_MODE) == "white"
+ assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_B94927/tele/STATE",
+ '{"POWER":"ON","Dimmer":50,"Color":"128,64,32,0","White":0}',
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("brightness") == 128
+ assert state.attributes.get("rgb_color") == (128, 64, 32)
+ assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgb"
+ assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
+
+
async def test_effect(hass, mqtt_mock):
"""Test effect."""
config = {
@@ -2569,6 +2716,13 @@ 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, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py
index f4bf11df026..8aba08f60d7 100644
--- a/tests/components/mqtt/test_light_json.py
+++ b/tests/components/mqtt/test_light_json.py
@@ -93,6 +93,9 @@ from unittest.mock import call, patch
import pytest
from homeassistant.components import light
+from homeassistant.components.mqtt.light.schema_basic import (
+ MQTT_LIGHT_ATTRIBUTES_BLOCKED,
+)
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_SUPPORTED_FEATURES,
@@ -121,6 +124,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -1714,6 +1718,13 @@ 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, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py
index 2e726d40ef1..2dabf0b7e46 100644
--- a/tests/components/mqtt/test_light_template.py
+++ b/tests/components/mqtt/test_light_template.py
@@ -31,6 +31,9 @@ from unittest.mock import patch
import pytest
from homeassistant.components import light
+from homeassistant.components.mqtt.light.schema_basic import (
+ MQTT_LIGHT_ATTRIBUTES_BLOCKED,
+)
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_SUPPORTED_FEATURES,
@@ -59,6 +62,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -870,6 +874,13 @@ 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, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py
index 754f60f49b2..39250b0a2fa 100644
--- a/tests/components/mqtt/test_lock.py
+++ b/tests/components/mqtt/test_lock.py
@@ -10,6 +10,7 @@ from homeassistant.components.lock import (
STATE_LOCKED,
STATE_UNLOCKED,
)
+from homeassistant.components.mqtt.lock import MQTT_LOCK_ATTRIBUTES_BLOCKED
from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID
from homeassistant.setup import async_setup_component
@@ -32,6 +33,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -311,6 +313,13 @@ 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, LOCK_DOMAIN, DEFAULT_CONFIG, MQTT_LOCK_ATTRIBUTES_BLOCKED
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py
index d93b0483865..37693340308 100644
--- a/tests/components/mqtt/test_number.py
+++ b/tests/components/mqtt/test_number.py
@@ -5,7 +5,11 @@ from unittest.mock import patch
import pytest
from homeassistant.components import number
-from homeassistant.components.mqtt.number import CONF_MAX, CONF_MIN
+from homeassistant.components.mqtt.number import (
+ CONF_MAX,
+ CONF_MIN,
+ MQTT_NUMBER_ATTRIBUTES_BLOCKED,
+)
from homeassistant.components.number import (
ATTR_MAX,
ATTR_MIN,
@@ -37,6 +41,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -81,6 +86,39 @@ async def test_run_number_setup(hass, mqtt_mock):
assert state.state == "20.5"
+async def test_value_template(hass, mqtt_mock):
+ """Test that it fetches the given payload with a template."""
+ topic = "test/number"
+ await async_setup_component(
+ hass,
+ "number",
+ {
+ "number": {
+ "platform": "mqtt",
+ "state_topic": topic,
+ "command_topic": topic,
+ "name": "Test Number",
+ "value_template": "{{ value_json.val }}",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, topic, '{"val":10}')
+
+ await hass.async_block_till_done()
+
+ state = hass.states.get("number.test_number")
+ assert state.state == "10"
+
+ async_fire_mqtt_message(hass, topic, '{"val":20.5}')
+
+ await hass.async_block_till_done()
+
+ state = hass.states.get("number.test_number")
+ assert state.state == "20.5"
+
+
async def test_run_number_service_optimistic(hass, mqtt_mock):
"""Test that set_value service works in optimistic mode."""
topic = "test/number"
@@ -217,6 +255,13 @@ 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, number.DOMAIN, DEFAULT_CONFIG, MQTT_NUMBER_ATTRIBUTES_BLOCKED
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
@@ -359,7 +404,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock):
async def test_entity_debug_info_message(hass, mqtt_mock):
"""Test MQTT debug info."""
await help_test_entity_debug_info_message(
- hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, payload=b"1"
+ hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, payload="1"
)
@@ -408,7 +453,7 @@ async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock):
)
await hass.async_block_till_done()
- assert f"'{CONF_MAX}'' must be > '{CONF_MIN}'" in caplog.text
+ assert f"'{CONF_MAX}' must be > '{CONF_MIN}'" in caplog.text
async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock):
diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py
new file mode 100644
index 00000000000..5dad989a5cf
--- /dev/null
+++ b/tests/components/mqtt/test_select.py
@@ -0,0 +1,452 @@
+"""The tests for mqtt select component."""
+import json
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant.components import select
+from homeassistant.components.mqtt.select import (
+ CONF_OPTIONS,
+ MQTT_SELECT_ATTRIBUTES_BLOCKED,
+)
+from homeassistant.components.select import (
+ ATTR_OPTION,
+ ATTR_OPTIONS,
+ DOMAIN as SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+)
+from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID
+import homeassistant.core as ha
+from homeassistant.setup import async_setup_component
+
+from .test_common import (
+ help_test_availability_when_connection_lost,
+ help_test_availability_without_topic,
+ help_test_custom_availability_payload,
+ help_test_default_availability_payload,
+ help_test_discovery_broken,
+ help_test_discovery_removal,
+ help_test_discovery_update,
+ help_test_discovery_update_attr,
+ help_test_discovery_update_unchanged,
+ help_test_entity_debug_info_message,
+ help_test_entity_device_info_remove,
+ help_test_entity_device_info_update,
+ help_test_entity_device_info_with_connection,
+ help_test_entity_device_info_with_identifier,
+ help_test_entity_id_update_discovery_update,
+ help_test_entity_id_update_subscriptions,
+ help_test_setting_attribute_via_mqtt_json_message,
+ help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
+ help_test_unique_id,
+ help_test_update_with_json_attrs_bad_JSON,
+ help_test_update_with_json_attrs_not_dict,
+)
+
+from tests.common import async_fire_mqtt_message
+
+DEFAULT_CONFIG = {
+ select.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "test-topic",
+ "options": ["milk", "beer"],
+ }
+}
+
+
+async def test_run_select_setup(hass, mqtt_mock):
+ """Test that it fetches the given payload."""
+ topic = "test/select"
+ await async_setup_component(
+ hass,
+ "select",
+ {
+ "select": {
+ "platform": "mqtt",
+ "state_topic": topic,
+ "command_topic": topic,
+ "name": "Test Select",
+ "options": ["milk", "beer"],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, topic, "milk")
+
+ await hass.async_block_till_done()
+
+ state = hass.states.get("select.test_select")
+ assert state.state == "milk"
+
+ async_fire_mqtt_message(hass, topic, "beer")
+
+ await hass.async_block_till_done()
+
+ state = hass.states.get("select.test_select")
+ assert state.state == "beer"
+
+
+async def test_value_template(hass, mqtt_mock):
+ """Test that it fetches the given payload with a template."""
+ topic = "test/select"
+ await async_setup_component(
+ hass,
+ "select",
+ {
+ "select": {
+ "platform": "mqtt",
+ "state_topic": topic,
+ "command_topic": topic,
+ "name": "Test Select",
+ "options": ["milk", "beer"],
+ "value_template": "{{ value_json.val }}",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, topic, '{"val":"milk"}')
+
+ await hass.async_block_till_done()
+
+ state = hass.states.get("select.test_select")
+ assert state.state == "milk"
+
+ async_fire_mqtt_message(hass, topic, '{"val":"beer"}')
+
+ await hass.async_block_till_done()
+
+ state = hass.states.get("select.test_select")
+ assert state.state == "beer"
+
+
+async def test_run_select_service_optimistic(hass, mqtt_mock):
+ """Test that set_value service works in optimistic mode."""
+ topic = "test/select"
+
+ fake_state = ha.State("select.test", "milk")
+
+ with patch(
+ "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state",
+ return_value=fake_state,
+ ):
+ assert await async_setup_component(
+ hass,
+ select.DOMAIN,
+ {
+ "select": {
+ "platform": "mqtt",
+ "command_topic": topic,
+ "name": "Test Select",
+ "options": ["milk", "beer"],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("select.test_select")
+ assert state.state == "milk"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {ATTR_ENTITY_ID: "select.test_select", ATTR_OPTION: "beer"},
+ blocking=True,
+ )
+
+ mqtt_mock.async_publish.assert_called_once_with(topic, "beer", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("select.test_select")
+ assert state.state == "beer"
+
+
+async def test_run_select_service(hass, mqtt_mock):
+ """Test that set_value service works in non optimistic mode."""
+ cmd_topic = "test/select/set"
+ state_topic = "test/select"
+
+ assert await async_setup_component(
+ hass,
+ select.DOMAIN,
+ {
+ "select": {
+ "platform": "mqtt",
+ "command_topic": cmd_topic,
+ "state_topic": state_topic,
+ "name": "Test Select",
+ "options": ["milk", "beer"],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, state_topic, "beer")
+ state = hass.states.get("select.test_select")
+ assert state.state == "beer"
+
+ await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {ATTR_ENTITY_ID: "select.test_select", ATTR_OPTION: "milk"},
+ blocking=True,
+ )
+ mqtt_mock.async_publish.assert_called_once_with(cmd_topic, "milk", 0, False)
+ state = hass.states.get("select.test_select")
+ assert state.state == "beer"
+
+
+async def test_availability_when_connection_lost(hass, mqtt_mock):
+ """Test availability after MQTT disconnection."""
+ await help_test_availability_when_connection_lost(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_availability_without_topic(hass, mqtt_mock):
+ """Test availability without defined availability topic."""
+ await help_test_availability_without_topic(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_default_availability_payload(hass, mqtt_mock):
+ """Test availability by default payload with defined topic."""
+ await help_test_default_availability_payload(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_custom_availability_payload(hass, mqtt_mock):
+ """Test availability by custom payload with defined topic."""
+ await help_test_custom_availability_payload(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock):
+ """Test the setting of attribute via MQTT with JSON payload."""
+ await help_test_setting_attribute_via_mqtt_json_message(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+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, select.DOMAIN, DEFAULT_CONFIG, MQTT_SELECT_ATTRIBUTES_BLOCKED
+ )
+
+
+async def test_setting_attribute_with_template(hass, mqtt_mock):
+ """Test the setting of attribute via MQTT with JSON payload."""
+ await help_test_setting_attribute_with_template(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog):
+ """Test attributes get extracted from a JSON result."""
+ await help_test_update_with_json_attrs_not_dict(
+ hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog):
+ """Test attributes get extracted from a JSON result."""
+ await help_test_update_with_json_attrs_bad_JSON(
+ hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_discovery_update_attr(hass, mqtt_mock, caplog):
+ """Test update of discovered MQTTAttributes."""
+ await help_test_discovery_update_attr(
+ hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_unique_id(hass, mqtt_mock):
+ """Test unique id option only creates one select per unique_id."""
+ config = {
+ select.DOMAIN: [
+ {
+ "platform": "mqtt",
+ "name": "Test 1",
+ "state_topic": "test-topic",
+ "command_topic": "test-topic",
+ "unique_id": "TOTALLY_UNIQUE",
+ "options": ["milk", "beer"],
+ },
+ {
+ "platform": "mqtt",
+ "name": "Test 2",
+ "state_topic": "test-topic",
+ "command_topic": "test-topic",
+ "unique_id": "TOTALLY_UNIQUE",
+ "options": ["milk", "beer"],
+ },
+ ]
+ }
+ await help_test_unique_id(hass, mqtt_mock, select.DOMAIN, config)
+
+
+async def test_discovery_removal_select(hass, mqtt_mock, caplog):
+ """Test removal of discovered select."""
+ data = json.dumps(DEFAULT_CONFIG[select.DOMAIN])
+ await help_test_discovery_removal(hass, mqtt_mock, caplog, select.DOMAIN, data)
+
+
+async def test_discovery_update_select(hass, mqtt_mock, caplog):
+ """Test update of discovered select."""
+ data1 = '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}'
+ data2 = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}'
+
+ await help_test_discovery_update(
+ hass, mqtt_mock, caplog, select.DOMAIN, data1, data2
+ )
+
+
+async def test_discovery_update_unchanged_select(hass, mqtt_mock, caplog):
+ """Test update of discovered select."""
+ data1 = '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}'
+ with patch(
+ "homeassistant.components.mqtt.select.MqttSelect.discovery_update"
+ ) as discovery_update:
+ await help_test_discovery_update_unchanged(
+ hass, mqtt_mock, caplog, select.DOMAIN, data1, discovery_update
+ )
+
+
+@pytest.mark.no_fail_on_log_exception
+async def test_discovery_broken(hass, mqtt_mock, caplog):
+ """Test handling of bad discovery message."""
+ data1 = '{ "name": "Beer" }'
+ data2 = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}'
+
+ await help_test_discovery_broken(
+ hass, mqtt_mock, caplog, select.DOMAIN, data1, data2
+ )
+
+
+async def test_entity_device_info_with_connection(hass, mqtt_mock):
+ """Test MQTT select device registry integration."""
+ await help_test_entity_device_info_with_connection(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_entity_device_info_with_identifier(hass, mqtt_mock):
+ """Test MQTT select device registry integration."""
+ await help_test_entity_device_info_with_identifier(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_entity_device_info_update(hass, mqtt_mock):
+ """Test device registry update."""
+ await help_test_entity_device_info_update(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_entity_device_info_remove(hass, mqtt_mock):
+ """Test device registry remove."""
+ await help_test_entity_device_info_remove(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_entity_id_update_subscriptions(hass, mqtt_mock):
+ """Test MQTT subscriptions are managed when entity_id is updated."""
+ await help_test_entity_id_update_subscriptions(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_entity_id_update_discovery_update(hass, mqtt_mock):
+ """Test MQTT discovery update when entity_id is updated."""
+ await help_test_entity_id_update_discovery_update(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_entity_debug_info_message(hass, mqtt_mock):
+ """Test MQTT debug info."""
+ await help_test_entity_debug_info_message(
+ hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG, payload="milk"
+ )
+
+
+async def test_options_attributes(hass, mqtt_mock):
+ """Test options attribute."""
+ topic = "test/select"
+ await async_setup_component(
+ hass,
+ "select",
+ {
+ "select": {
+ "platform": "mqtt",
+ "state_topic": topic,
+ "command_topic": topic,
+ "name": "Test select",
+ "options": ["milk", "beer"],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("select.test_select")
+ assert state.attributes.get(ATTR_OPTIONS) == ["milk", "beer"]
+
+
+async def test_invalid_options(hass, caplog, mqtt_mock):
+ """Test invalid options."""
+ topic = "test/select"
+ await async_setup_component(
+ hass,
+ "select",
+ {
+ "select": {
+ "platform": "mqtt",
+ "state_topic": topic,
+ "command_topic": topic,
+ "name": "Test Select",
+ "options": "beer",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert f"'{CONF_OPTIONS}' must include at least 2 options" in caplog.text
+
+
+async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock):
+ """Test warning for MQTT payload which is not a valid option."""
+ topic = "test/select"
+ await async_setup_component(
+ hass,
+ "select",
+ {
+ "select": {
+ "platform": "mqtt",
+ "state_topic": topic,
+ "command_topic": topic,
+ "name": "Test Select",
+ "options": ["milk", "beer"],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, topic, "öl")
+
+ await hass.async_block_till_done()
+
+ assert (
+ "Invalid option for select.test_select: 'öl' (valid options: ['milk', 'beer'])"
+ in caplog.text
+ )
diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py
index 7d732849906..15ca9870077 100644
--- a/tests/components/mqtt/test_sensor.py
+++ b/tests/components/mqtt/test_sensor.py
@@ -6,6 +6,7 @@ from unittest.mock import patch
import pytest
+from homeassistant.components.mqtt.sensor import MQTT_SENSOR_ATTRIBUTES_BLOCKED
import homeassistant.components.sensor as sensor
from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE
import homeassistant.core as ha
@@ -42,6 +43,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -531,6 +533,13 @@ 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, sensor.DOMAIN, DEFAULT_CONFIG, MQTT_SENSOR_ATTRIBUTES_BLOCKED
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py
index e18b0b05835..46cc5552b6a 100644
--- a/tests/components/mqtt/test_state_vacuum.py
+++ b/tests/components/mqtt/test_state_vacuum.py
@@ -8,6 +8,7 @@ import pytest
from homeassistant.components import vacuum
from homeassistant.components.mqtt import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC
from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum
+from homeassistant.components.mqtt.vacuum.const import MQTT_VACUUM_ATTRIBUTES_BLOCKED
from homeassistant.components.mqtt.vacuum.schema import services_to_strings
from homeassistant.components.mqtt.vacuum.schema_state import SERVICE_TO_STRING
from homeassistant.components.vacuum import (
@@ -52,6 +53,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -359,6 +361,13 @@ 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, vacuum.DOMAIN, DEFAULT_CONFIG_2, MQTT_VACUUM_ATTRIBUTES_BLOCKED
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py
index 607e4468a5f..d5dcfbd4fa7 100644
--- a/tests/components/mqtt/test_switch.py
+++ b/tests/components/mqtt/test_switch.py
@@ -6,6 +6,7 @@ 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, STATE_OFF, STATE_ON
import homeassistant.core as ha
from homeassistant.setup import async_setup_component
@@ -29,6 +30,7 @@ from .test_common import (
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template,
+ help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
@@ -246,6 +248,13 @@ 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
+ )
+
+
async def test_setting_attribute_with_template(hass, mqtt_mock):
"""Test the setting of attribute via MQTT with JSON payload."""
await help_test_setting_attribute_with_template(
diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py
index 7a4733e8ce2..8fbe9486352 100644
--- a/tests/components/mysensors/conftest.py
+++ b/tests/components/mysensors/conftest.py
@@ -1,10 +1,158 @@
"""Provide common mysensors fixtures."""
+from __future__ import annotations
+
+from collections.abc import AsyncGenerator, Generator
+import json
+from typing import Any
+from unittest.mock import MagicMock, patch
+
+from mysensors.persistence import MySensorsJSONDecoder
+from mysensors.sensor import Sensor
import pytest
from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN
+from homeassistant.components.mysensors import CONF_VERSION, DEFAULT_BAUD_RATE
+from homeassistant.components.mysensors.const import (
+ CONF_BAUD_RATE,
+ CONF_DEVICE,
+ CONF_GATEWAY_TYPE,
+ CONF_GATEWAY_TYPE_SERIAL,
+ CONF_GATEWAYS,
+ DOMAIN,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry, load_fixture
+
+
+@pytest.fixture(autouse=True)
+def device_tracker_storage(mock_device_tracker_conf):
+ """Mock out device tracker known devices storage."""
+ devices = mock_device_tracker_conf
+ return devices
@pytest.fixture(name="mqtt")
-async def mock_mqtt_fixture(hass):
+def mock_mqtt_fixture(hass) -> None:
"""Mock the MQTT integration."""
hass.config.components.add(MQTT_DOMAIN)
+
+
+@pytest.fixture(name="is_serial_port")
+def is_serial_port_fixture() -> Generator[MagicMock, None, None]:
+ """Patch the serial port check."""
+ with patch("homeassistant.components.mysensors.gateway.cv.isdevice") as is_device:
+ is_device.side_effect = lambda device: device
+ yield is_device
+
+
+@pytest.fixture(name="gateway_nodes")
+def gateway_nodes_fixture() -> dict[int, Sensor]:
+ """Return the gateway nodes dict."""
+ return {}
+
+
+@pytest.fixture(name="serial_transport")
+async def serial_transport_fixture(
+ gateway_nodes: dict[int, Sensor],
+ is_serial_port: MagicMock,
+) -> AsyncGenerator[dict[int, Sensor], None]:
+ """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
+
+ mock_gateway_features(tasks, transport_class, gateway_nodes)
+
+ yield transport_class
+
+
+def mock_gateway_features(
+ tasks: MagicMock, transport_class: MagicMock, nodes: dict[int, Sensor]
+) -> None:
+ """Mock the gateway features."""
+
+ async def mock_start_persistence():
+ """Load nodes from via persistence."""
+ gateway = transport_class.call_args[0][0]
+ gateway.sensors.update(nodes)
+
+ tasks.start_persistence.side_effect = mock_start_persistence
+
+ async def mock_start():
+ """Mock the start method."""
+ gateway = transport_class.call_args[0][0]
+ gateway.on_conn_made(gateway)
+
+ tasks.start.side_effect = mock_start
+
+
+@pytest.fixture(name="transport")
+def transport_fixture(serial_transport: MagicMock) -> MagicMock:
+ """Return the default mocked transport."""
+ return serial_transport
+
+
+@pytest.fixture(name="serial_entry")
+async def serial_entry_fixture(hass) -> MockConfigEntry:
+ """Create a config entry for a serial gateway."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL,
+ CONF_VERSION: "2.3",
+ CONF_DEVICE: "/test/device",
+ CONF_BAUD_RATE: DEFAULT_BAUD_RATE,
+ },
+ )
+ return entry
+
+
+@pytest.fixture(name="config_entry")
+def config_entry_fixture(serial_entry: MockConfigEntry) -> MockConfigEntry:
+ """Provide the config entry used for integration set up."""
+ return serial_entry
+
+
+@pytest.fixture
+async def integration(
+ hass: HomeAssistant, transport: MagicMock, config_entry: MockConfigEntry
+) -> 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)
+ 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
+
+
+def load_nodes_state(fixture_path: str) -> dict:
+ """Load mysensors nodes fixture."""
+ return json.loads(load_fixture(fixture_path), cls=MySensorsJSONDecoder)
+
+
+def update_gateway_nodes(
+ gateway_nodes: dict[int, Sensor], nodes: dict[int, Sensor]
+) -> dict:
+ """Update the gateway nodes."""
+ gateway_nodes.update(nodes)
+ return nodes
+
+
+@pytest.fixture(name="gps_sensor_state", scope="session")
+def gps_sensor_state_fixture() -> dict:
+ """Load the gps sensor state."""
+ return load_nodes_state("mysensors/gps_sensor_state.json")
+
+
+@pytest.fixture
+def gps_sensor(gateway_nodes, gps_sensor_state) -> Sensor:
+ """Load the gps sensor."""
+ nodes = update_gateway_nodes(gateway_nodes, gps_sensor_state)
+ node = nodes[1]
+ return node
diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py
index 161d00e44b3..daee8a37eba 100644
--- a/tests/components/mysensors/test_config_flow.py
+++ b/tests/components/mysensors/test_config_flow.py
@@ -25,13 +25,14 @@ from homeassistant.components.mysensors.const import (
ConfGatewayType,
)
from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import FlowResult
from tests.common import MockConfigEntry
async def get_form(
hass: HomeAssistant, gatway_type: ConfGatewayType, expected_step_id: str
-):
+) -> FlowResult:
"""Get a form for the given gateway type."""
await setup.async_setup_component(hass, "persistent_notification", {})
stepuser = await hass.config_entries.flow.async_init(
@@ -107,7 +108,7 @@ async def test_missing_mqtt(hass: HomeAssistant) -> None:
assert result["errors"] == {"base": "mqtt_required"}
-async def test_config_serial(hass: HomeAssistant):
+async def test_config_serial(hass: HomeAssistant) -> None:
"""Test configuring a gateway via serial."""
step = await get_form(hass, CONF_GATEWAY_TYPE_SERIAL, "gw_serial")
flow_id = step["flow_id"]
@@ -147,7 +148,7 @@ async def test_config_serial(hass: HomeAssistant):
assert len(mock_setup_entry.mock_calls) == 1
-async def test_config_tcp(hass: HomeAssistant):
+async def test_config_tcp(hass: HomeAssistant) -> None:
"""Test configuring a gateway via tcp."""
step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp")
flow_id = step["flow_id"]
@@ -184,7 +185,7 @@ async def test_config_tcp(hass: HomeAssistant):
assert len(mock_setup_entry.mock_calls) == 1
-async def test_fail_to_connect(hass: HomeAssistant):
+async def test_fail_to_connect(hass: HomeAssistant) -> None:
"""Test configuring a gateway via tcp."""
step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp")
flow_id = step["flow_id"]
@@ -209,8 +210,9 @@ async def test_fail_to_connect(hass: HomeAssistant):
assert result2["type"] == "form"
assert "errors" in result2
- assert "base" in result2["errors"]
- assert result2["errors"]["base"] == "cannot_connect"
+ errors = result2["errors"]
+ assert errors
+ assert errors.get("base") == "cannot_connect"
assert len(mock_setup.mock_calls) == 0
assert len(mock_setup_entry.mock_calls) == 0
@@ -262,16 +264,6 @@ async def test_fail_to_connect(hass: HomeAssistant):
CONF_VERSION,
"invalid_version",
),
- (
- CONF_GATEWAY_TYPE_TCP,
- "gw_tcp",
- {
- CONF_TCP_PORT: 5003,
- CONF_DEVICE: "127.0.0.1",
- },
- CONF_VERSION,
- "invalid_version",
- ),
(
CONF_GATEWAY_TYPE_TCP,
"gw_tcp",
@@ -300,6 +292,7 @@ async def test_fail_to_connect(hass: HomeAssistant):
{
CONF_TCP_PORT: 5003,
CONF_DEVICE: "127.0.0.",
+ CONF_VERSION: "2.4",
},
CONF_DEVICE,
"invalid_ip",
@@ -310,6 +303,7 @@ async def test_fail_to_connect(hass: HomeAssistant):
{
CONF_TCP_PORT: 5003,
CONF_DEVICE: "abcd",
+ CONF_VERSION: "2.4",
},
CONF_DEVICE,
"invalid_ip",
@@ -367,12 +361,12 @@ async def test_fail_to_connect(hass: HomeAssistant):
)
async def test_config_invalid(
hass: HomeAssistant,
- mqtt: config_entries.ConfigEntry,
+ mqtt: None,
gateway_type: ConfGatewayType,
expected_step_id: str,
user_input: dict[str, Any],
- err_field,
- err_string,
+ err_field: str,
+ err_string: str,
) -> None:
"""Perform a test that is expected to generate an error."""
step = await get_form(hass, gateway_type, expected_step_id)
@@ -397,8 +391,10 @@ async def test_config_invalid(
assert result2["type"] == "form"
assert "errors" in result2
- assert err_field in result2["errors"]
- assert result2["errors"][err_field] == err_string
+ errors = result2["errors"]
+ assert errors
+ assert err_field in errors
+ assert errors[err_field] == err_string
assert len(mock_setup.mock_calls) == 0
assert len(mock_setup_entry.mock_calls) == 0
diff --git a/tests/components/mysensors/test_gateway.py b/tests/components/mysensors/test_gateway.py
index f2e7aa77c8c..0c9652bdfc1 100644
--- a/tests/components/mysensors/test_gateway.py
+++ b/tests/components/mysensors/test_gateway.py
@@ -18,7 +18,9 @@ from homeassistant.core import HomeAssistant
("/dev/ttyACM0", False),
],
)
-def test_is_serial_port_windows(hass: HomeAssistant, port: str, expect_valid: bool):
+def test_is_serial_port_windows(
+ hass: HomeAssistant, port: str, expect_valid: bool
+) -> None:
"""Test windows serial port."""
with patch("sys.platform", "win32"):
diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py
new file mode 100644
index 00000000000..69caeac9977
--- /dev/null
+++ b/tests/components/mysensors/test_sensor.py
@@ -0,0 +1,10 @@
+"""Provide tests for mysensors sensor platform."""
+
+
+async def test_gps_sensor(hass, gps_sensor, integration):
+ """Test a gps sensor."""
+ entity_id = "sensor.gps_sensor_1_1"
+
+ state = hass.states.get(entity_id)
+
+ assert state.state == "40.741894,-73.989311,12"
diff --git a/tests/components/nam/test_air_quality.py b/tests/components/nam/test_air_quality.py
deleted file mode 100644
index f9a213cec3e..00000000000
--- a/tests/components/nam/test_air_quality.py
+++ /dev/null
@@ -1,148 +0,0 @@
-"""Test air_quality of Nettigo Air Monitor integration."""
-from datetime import timedelta
-from unittest.mock import patch
-
-from nettigo_air_monitor import ApiError
-
-from homeassistant.components.air_quality import ATTR_CO2, ATTR_PM_2_5, ATTR_PM_10
-from homeassistant.const import (
- ATTR_ENTITY_ID,
- ATTR_UNIT_OF_MEASUREMENT,
- CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
- STATE_UNAVAILABLE,
-)
-from homeassistant.helpers import entity_registry as er
-from homeassistant.setup import async_setup_component
-from homeassistant.util.dt import utcnow
-
-from . import INCOMPLETE_NAM_DATA, nam_data
-
-from tests.common import async_fire_time_changed
-from tests.components.nam import init_integration
-
-
-async def test_air_quality(hass):
- """Test states of the air_quality."""
- await init_integration(hass)
- registry = er.async_get(hass)
-
- state = hass.states.get("air_quality.nettigo_air_monitor_sds011")
- assert state
- assert state.state == "11"
- assert state.attributes.get(ATTR_PM_10) == 19
- assert state.attributes.get(ATTR_PM_2_5) == 11
- assert state.attributes.get(ATTR_CO2) == 865
- assert (
- state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
- == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
- )
-
- entry = registry.async_get("air_quality.nettigo_air_monitor_sds011")
- assert entry
- assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds"
-
- state = hass.states.get("air_quality.nettigo_air_monitor_sps30")
- assert state
- assert state.state == "34"
- assert state.attributes.get(ATTR_PM_10) == 21
- assert state.attributes.get(ATTR_PM_2_5) == 34
- assert state.attributes.get(ATTR_CO2) == 865
- assert (
- state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
- == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
- )
-
- entry = registry.async_get("air_quality.nettigo_air_monitor_sps30")
- assert entry
- assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30"
-
-
-async def test_air_quality_without_co2_value(hass):
- """Test states of the air_quality."""
- await init_integration(hass, co2_sensor=False)
-
- state = hass.states.get("air_quality.nettigo_air_monitor_sds011")
- assert state
- assert state.attributes.get(ATTR_CO2) is None
-
-
-async def test_incompleta_data_after_device_restart(hass):
- """Test states of the air_quality after device restart."""
- await init_integration(hass)
-
- state = hass.states.get("air_quality.nettigo_air_monitor_sds011")
- assert state
- assert state.state == "11"
- assert state.attributes.get(ATTR_PM_10) == 19
- assert state.attributes.get(ATTR_PM_2_5) == 11
- assert (
- state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
- == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
- )
-
- future = utcnow() + timedelta(minutes=6)
- with patch(
- "homeassistant.components.nam.NettigoAirMonitor._async_get_data",
- return_value=INCOMPLETE_NAM_DATA,
- ):
- async_fire_time_changed(hass, future)
- await hass.async_block_till_done()
-
- state = hass.states.get("air_quality.nettigo_air_monitor_sds011")
- assert state
- assert state.state == STATE_UNAVAILABLE
-
-
-async def test_availability(hass):
- """Ensure that we mark the entities unavailable correctly when device causes an error."""
- await init_integration(hass)
-
- state = hass.states.get("air_quality.nettigo_air_monitor_sds011")
- assert state
- assert state.state != STATE_UNAVAILABLE
- assert state.state == "11"
-
- future = utcnow() + timedelta(minutes=6)
- with patch(
- "homeassistant.components.nam.NettigoAirMonitor._async_get_data",
- side_effect=ApiError("API Error"),
- ):
- async_fire_time_changed(hass, future)
- await hass.async_block_till_done()
-
- state = hass.states.get("air_quality.nettigo_air_monitor_sds011")
- assert state
- assert state.state == STATE_UNAVAILABLE
-
- future = utcnow() + timedelta(minutes=12)
- with patch(
- "homeassistant.components.nam.NettigoAirMonitor._async_get_data",
- return_value=nam_data,
- ):
- async_fire_time_changed(hass, future)
- await hass.async_block_till_done()
-
- state = hass.states.get("air_quality.nettigo_air_monitor_sds011")
- assert state
- assert state.state != STATE_UNAVAILABLE
- assert state.state == "11"
-
-
-async def test_manual_update_entity(hass):
- """Test manual update entity via service homeasasistant/update_entity."""
- await init_integration(hass)
-
- await async_setup_component(hass, "homeassistant", {})
-
- with patch(
- "homeassistant.components.nam.NettigoAirMonitor._async_get_data",
- return_value=nam_data,
- ) as mock_get_data:
- await hass.services.async_call(
- "homeassistant",
- "update_entity",
- {ATTR_ENTITY_ID: ["air_quality.nettigo_air_monitor_sds011"]},
- blocking=True,
- )
-
- assert mock_get_data.call_count == 1
diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py
index 943ea53f360..97392cbaff8 100644
--- a/tests/components/nam/test_init.py
+++ b/tests/components/nam/test_init.py
@@ -3,9 +3,11 @@ from unittest.mock import patch
from nettigo_air_monitor import ApiError
+from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
from homeassistant.components.nam.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
from tests.components.nam import init_integration
@@ -15,7 +17,7 @@ async def test_async_setup_entry(hass):
"""Test a successful setup entry."""
await init_integration(hass)
- state = hass.states.get("air_quality.nettigo_air_monitor_sds011")
+ state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_2_5")
assert state is not None
assert state.state != STATE_UNAVAILABLE
assert state.state == "11"
@@ -51,3 +53,32 @@ async def test_unload_entry(hass):
assert entry.state is ConfigEntryState.NOT_LOADED
assert not hass.data.get(DOMAIN)
+
+
+async def test_remove_air_quality_entities(hass):
+ """Test remove air_quality entities from registry."""
+ registry = er.async_get(hass)
+
+ registry.async_get_or_create(
+ AIR_QUALITY_PLATFORM,
+ DOMAIN,
+ "aa:bb:cc:dd:ee:ff-sds011",
+ suggested_object_id="nettigo_air_monitor_sds011",
+ disabled_by=None,
+ )
+
+ registry.async_get_or_create(
+ AIR_QUALITY_PLATFORM,
+ DOMAIN,
+ "aa:bb:cc:dd:ee:ff-sps30",
+ suggested_object_id="nettigo_air_monitor_sps30",
+ disabled_by=None,
+ )
+
+ await init_integration(hass)
+
+ entry = registry.async_get("air_quality.nettigo_air_monitor_sds011")
+ assert entry is None
+
+ entry = registry.async_get("air_quality.nettigo_air_monitor_sps30")
+ assert entry is None
diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py
index b4c92c92e67..506a81f7619 100644
--- a/tests/components/nam/test_sensor.py
+++ b/tests/components/nam/test_sensor.py
@@ -13,7 +13,11 @@ from homeassistant.components.sensor import (
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
+ ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT,
+ CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ CONCENTRATION_PARTS_PER_MILLION,
+ DEVICE_CLASS_CO2,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_SIGNAL_STRENGTH,
@@ -143,7 +147,7 @@ async def test_sensor(hass):
entry = registry.async_get("sensor.nettigo_air_monitor_dht22_humidity")
assert entry
- assert entry.unique_id == "aa:bb:cc:dd:ee:ff-humidity"
+ assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_humidity"
state = hass.states.get("sensor.nettigo_air_monitor_dht22_temperature")
assert state
@@ -154,7 +158,7 @@ async def test_sensor(hass):
entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature")
assert entry
- assert entry.unique_id == "aa:bb:cc:dd:ee:ff-temperature"
+ assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_temperature"
state = hass.states.get("sensor.nettigo_air_monitor_heca_humidity")
assert state
@@ -205,6 +209,113 @@ async def test_sensor(hass):
assert entry
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-uptime"
+ state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_10")
+ assert state
+ assert state.state == "19"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:blur"
+
+ entry = registry.async_get(
+ "sensor.nettigo_air_monitor_sds011_particulate_matter_10"
+ )
+ assert entry
+ assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p1"
+
+ state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_2_5")
+ assert state
+ assert state.state == "11"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:blur"
+
+ entry = registry.async_get(
+ "sensor.nettigo_air_monitor_sds011_particulate_matter_2_5"
+ )
+ assert entry
+ assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p2"
+
+ state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_1_0")
+ assert state
+ assert state.state == "31"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:blur"
+
+ entry = registry.async_get(
+ "sensor.nettigo_air_monitor_sps30_particulate_matter_1_0"
+ )
+ assert entry
+ assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p0"
+
+ state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_10")
+ assert state
+ assert state.state == "21"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:blur"
+
+ entry = registry.async_get("sensor.nettigo_air_monitor_sps30_particulate_matter_10")
+ assert entry
+ assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p1"
+
+ state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_2_5")
+ assert state
+ assert state.state == "34"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:blur"
+
+ entry = registry.async_get(
+ "sensor.nettigo_air_monitor_sps30_particulate_matter_2_5"
+ )
+ assert entry
+ assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p2"
+
+ state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_4_0")
+ assert state
+ assert state.state == "25"
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:blur"
+
+ entry = registry.async_get(
+ "sensor.nettigo_air_monitor_sps30_particulate_matter_4_0"
+ )
+ assert entry
+ assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p4"
+
+ state = hass.states.get("sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide")
+ assert state
+ assert state.state == "865"
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO2
+ assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == CONCENTRATION_PARTS_PER_MILLION
+ )
+ entry = registry.async_get("sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide")
+ assert entry
+ assert entry.unique_id == "aa:bb:cc:dd:ee:ff-mhz14a_carbon_dioxide"
+
async def test_sensor_disabled(hass):
"""Test sensor disabled by default."""
@@ -302,3 +413,36 @@ async def test_manual_update_entity(hass):
)
assert mock_get_data.call_count == 1
+
+
+async def test_unique_id_migration(hass):
+ """Test states of the unique_id migration."""
+ registry = er.async_get(hass)
+
+ registry.async_get_or_create(
+ SENSOR_DOMAIN,
+ DOMAIN,
+ "aa:bb:cc:dd:ee:ff-temperature",
+ suggested_object_id="nettigo_air_monitor_dht22_temperature",
+ disabled_by=None,
+ )
+
+ registry.async_get_or_create(
+ SENSOR_DOMAIN,
+ DOMAIN,
+ "aa:bb:cc:dd:ee:ff-humidity",
+ suggested_object_id="nettigo_air_monitor_dht22_humidity",
+ disabled_by=None,
+ )
+
+ await init_integration(hass)
+
+ entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature")
+ assert entry
+ assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_temperature"
+
+ await init_integration(hass)
+
+ entry = registry.async_get("sensor.nettigo_air_monitor_dht22_humidity")
+ assert entry
+ assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_humidity"
diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py
index 32202cb85e5..5ba989e2504 100644
--- a/tests/components/netatmo/common.py
+++ b/tests/components/netatmo/common.py
@@ -7,6 +7,7 @@ from homeassistant.components.webhook import async_handle_webhook
from homeassistant.util.aiohttp import MockRequest
from tests.common import load_fixture
+from tests.test_util.aiohttp import AiohttpClientMockResponse
CLIENT_ID = "1234"
CLIENT_SECRET = "5678"
@@ -50,7 +51,7 @@ async def fake_post_request(*args, **kwargs):
if endpoint in "snapshot_720.jpg":
return b"test stream image bytes"
- if endpoint in [
+ elif endpoint in [
"setpersonsaway",
"setpersonshome",
"setstate",
@@ -58,9 +59,16 @@ async def fake_post_request(*args, **kwargs):
"setthermmode",
"switchhomeschedule",
]:
- return f'{{"{endpoint}": true}}'
+ payload = f'{{"{endpoint}": true}}'
- return json.loads(load_fixture(f"netatmo/{endpoint}.json"))
+ else:
+ payload = json.loads(load_fixture(f"netatmo/{endpoint}.json"))
+
+ return AiohttpClientMockResponse(
+ method="POST",
+ url=kwargs["url"],
+ json=payload,
+ )
async def fake_post_request_no_data(*args, **kwargs):
diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py
index 40143a67bac..c348c7adb9c 100644
--- a/tests/components/nsw_fuel_station/test_sensor.py
+++ b/tests/components/nsw_fuel_station/test_sensor.py
@@ -1,7 +1,10 @@
"""The tests for the NSW Fuel Station sensor platform."""
from unittest.mock import patch
+from nsw_fuel import FuelCheckError
+
from homeassistant.components import sensor
+from homeassistant.components.nsw_fuel_station import DOMAIN
from homeassistant.setup import async_setup_component
from tests.common import assert_setup_component
@@ -12,6 +15,8 @@ VALID_CONFIG = {
"fuel_types": ["E10", "P95"],
}
+VALID_CONFIG_EXPECTED_ENTITY_IDS = ["my_fake_station_p95", "my_fake_station_e10"]
+
class MockPrice:
"""Mock Price implementation."""
@@ -34,48 +39,41 @@ class MockStation:
self.code = code
-class MockGetReferenceDataResponse:
- """Mock GetReferenceDataResponse implementation."""
+class MockGetFuelPricesResponse:
+ """Mock GetFuelPricesResponse implementation."""
- def __init__(self, stations):
- """Initialize a mock GetReferenceDataResponse instance."""
+ def __init__(self, prices, stations):
+ """Initialize a mock GetFuelPricesResponse instance."""
+ self.prices = prices
self.stations = stations
-class FuelCheckClientMock:
- """Mock FuelCheckClient implementation."""
-
- def get_fuel_prices_for_station(self, station):
- """Return a fake fuel prices response."""
- return [
- MockPrice(
- price=150.0,
- fuel_type="P95",
- last_updated=None,
- price_unit=None,
- station_code=350,
- ),
- MockPrice(
- price=140.0,
- fuel_type="E10",
- last_updated=None,
- price_unit=None,
- station_code=350,
- ),
- ]
-
- def get_reference_data(self):
- """Return a fake reference data response."""
- return MockGetReferenceDataResponse(
- stations=[MockStation(code=350, name="My Fake Station")]
- )
+MOCK_FUEL_PRICES_RESPONSE = MockGetFuelPricesResponse(
+ prices=[
+ MockPrice(
+ price=150.0,
+ fuel_type="P95",
+ last_updated=None,
+ price_unit=None,
+ station_code=350,
+ ),
+ MockPrice(
+ price=140.0,
+ fuel_type="E10",
+ last_updated=None,
+ price_unit=None,
+ station_code=350,
+ ),
+ ],
+ stations=[MockStation(code=350, name="My Fake Station")],
+)
@patch(
- "homeassistant.components.nsw_fuel_station.sensor.FuelCheckClient",
- new=FuelCheckClientMock,
+ "homeassistant.components.nsw_fuel_station.FuelCheckClient.get_fuel_prices",
+ return_value=MOCK_FUEL_PRICES_RESPONSE,
)
-async def test_setup(hass):
+async def test_setup(get_fuel_prices, hass):
"""Test the setup with custom settings."""
with assert_setup_component(1, sensor.DOMAIN):
assert await async_setup_component(
@@ -83,19 +81,71 @@ async def test_setup(hass):
)
await hass.async_block_till_done()
- fake_entities = ["my_fake_station_p95", "my_fake_station_e10"]
-
- for entity_id in fake_entities:
+ for entity_id in VALID_CONFIG_EXPECTED_ENTITY_IDS:
state = hass.states.get(f"sensor.{entity_id}")
assert state is not None
+def raise_fuel_check_error():
+ """Raise fuel check error for testing error cases."""
+ raise FuelCheckError()
+
+
@patch(
- "homeassistant.components.nsw_fuel_station.sensor.FuelCheckClient",
- new=FuelCheckClientMock,
+ "homeassistant.components.nsw_fuel_station.FuelCheckClient.get_fuel_prices",
+ side_effect=raise_fuel_check_error,
)
-async def test_sensor_values(hass):
+async def test_setup_error(get_fuel_prices, hass):
+ """Test the setup with client throwing error."""
+ with assert_setup_component(1, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}
+ )
+ await hass.async_block_till_done()
+
+ for entity_id in VALID_CONFIG_EXPECTED_ENTITY_IDS:
+ state = hass.states.get(f"sensor.{entity_id}")
+ assert state is None
+
+
+@patch(
+ "homeassistant.components.nsw_fuel_station.FuelCheckClient.get_fuel_prices",
+ return_value=MOCK_FUEL_PRICES_RESPONSE,
+)
+async def test_setup_error_no_station(get_fuel_prices, hass):
+ """Test the setup with specified station not existing."""
+ with assert_setup_component(2, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": [
+ {
+ "platform": "nsw_fuel_station",
+ "station_id": 350,
+ "fuel_types": ["E10"],
+ },
+ {
+ "platform": "nsw_fuel_station",
+ "station_id": 351,
+ "fuel_types": ["P95"],
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("sensor.my_fake_station_e10") is not None
+ assert hass.states.get("sensor.my_fake_station_p95") is None
+
+
+@patch(
+ "homeassistant.components.nsw_fuel_station.FuelCheckClient.get_fuel_prices",
+ return_value=MOCK_FUEL_PRICES_RESPONSE,
+)
+async def test_sensor_values(get_fuel_prices, hass):
"""Test retrieval of sensor values."""
+ assert await async_setup_component(hass, DOMAIN, {})
assert await async_setup_component(hass, sensor.DOMAIN, {"sensor": VALID_CONFIG})
await hass.async_block_till_done()
diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py
index 626eec433d1..e4cb079515c 100644
--- a/tests/components/onvif/test_config_flow.py
+++ b/tests/components/onvif/test_config_flow.py
@@ -203,7 +203,7 @@ async def test_flow_discovered_devices(hass):
setup_mock_device(mock_device)
result = await hass.config_entries.flow.async_configure(
- result["flow_id"], user_input={}
+ result["flow_id"], user_input={"auto": True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -215,7 +215,7 @@ async def test_flow_discovered_devices(hass):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["step_id"] == "auth"
+ assert result["step_id"] == "configure"
with patch(
"homeassistant.components.onvif.async_setup", return_value=True
@@ -268,7 +268,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass):
setup_mock_device(mock_device)
result = await hass.config_entries.flow.async_configure(
- result["flow_id"], user_input={}
+ result["flow_id"], user_input={"auto": True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -281,7 +281,37 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["step_id"] == "manual_input"
+ assert result["step_id"] == "configure"
+
+
+async def test_flow_discovered_no_device(hass):
+ """Test that config flow discovery no device."""
+ await setup_onvif_integration(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ with patch(
+ "homeassistant.components.onvif.config_flow.get_device"
+ ) as mock_onvif_camera, patch(
+ "homeassistant.components.onvif.config_flow.wsdiscovery"
+ ) as mock_discovery, patch(
+ "homeassistant.components.onvif.ONVIFDevice"
+ ) as mock_device:
+ setup_mock_onvif_camera(mock_onvif_camera)
+ mock_discovery.return_value = []
+ setup_mock_device(mock_device)
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={"auto": True}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "configure"
async def test_flow_discovery_ignore_existing_and_abort(hass):
@@ -319,12 +349,12 @@ async def test_flow_discovery_ignore_existing_and_abort(hass):
setup_mock_device(mock_device)
result = await hass.config_entries.flow.async_configure(
- result["flow_id"], user_input={}
+ result["flow_id"], user_input={"auto": True}
)
# It should skip to manual entry if the only devices are already configured
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["step_id"] == "manual_input"
+ assert result["step_id"] == "configure"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -332,15 +362,6 @@ async def test_flow_discovery_ignore_existing_and_abort(hass):
config_flow.CONF_NAME: NAME,
config_flow.CONF_HOST: HOST,
config_flow.CONF_PORT: PORT,
- },
- )
-
- assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["step_id"] == "auth"
-
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={
config_flow.CONF_USERNAME: USERNAME,
config_flow.CONF_PASSWORD: PASSWORD,
},
@@ -373,23 +394,11 @@ async def test_flow_manual_entry(hass):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- user_input={},
+ user_input={"auto": False},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["step_id"] == "manual_input"
-
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={
- config_flow.CONF_NAME: NAME,
- config_flow.CONF_HOST: HOST,
- config_flow.CONF_PORT: PORT,
- },
- )
-
- assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["step_id"] == "auth"
+ assert result["step_id"] == "configure"
with patch(
"homeassistant.components.onvif.async_setup", return_value=True
@@ -399,6 +408,9 @@ async def test_flow_manual_entry(hass):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
+ config_flow.CONF_NAME: NAME,
+ config_flow.CONF_HOST: HOST,
+ config_flow.CONF_PORT: PORT,
config_flow.CONF_USERNAME: USERNAME,
config_flow.CONF_PASSWORD: PASSWORD,
},
@@ -598,7 +610,7 @@ async def test_flow_import_onvif_auth_error(hass):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["step_id"] == "auth"
+ assert result["step_id"] == "configure"
assert result["errors"]["base"] == "cannot_connect"
diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py
index ba1be4afb4c..5225aad83cd 100644
--- a/tests/components/openweathermap/test_config_flow.py
+++ b/tests/components/openweathermap/test_config_flow.py
@@ -187,6 +187,7 @@ def _create_mocked_owm(is_api_online: bool):
weather.snow.return_value = []
weather.detailed_status.return_value = "status"
weather.weather_code = 803
+ weather.dewpoint = 10
mocked_owm.weather_at_coords.return_value.weather = weather
diff --git a/tests/components/ozw/test_light.py b/tests/components/ozw/test_light.py
index 6388629be8c..a8ed4352f9a 100644
--- a/tests/components/ozw/test_light.py
+++ b/tests/components/ozw/test_light.py
@@ -1,4 +1,5 @@
"""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
@@ -12,6 +13,8 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
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
@@ -51,6 +54,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
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
@@ -119,6 +123,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
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
@@ -183,6 +188,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
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]
@@ -216,6 +222,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
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]
@@ -249,6 +256,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
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]
@@ -282,6 +290,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
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
@@ -315,6 +324,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
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
@@ -348,6 +358,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
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(
@@ -360,7 +371,9 @@ async def test_pure_rgb_dimmer_light(
state = hass.states.get("light.kitchen_rgb_strip_level")
assert state is not None
assert state.state == "on"
- assert state.attributes["supported_features"] == 17
+ 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]
@@ -390,6 +403,7 @@ async def test_pure_rgb_dimmer_light(
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):
@@ -400,6 +414,8 @@ async def test_no_rgb_light(hass, light_data, light_no_rgb_msg, sent_messages):
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
@@ -429,6 +445,7 @@ async def test_no_rgb_light(hass, light_data, light_no_rgb_msg, sent_messages):
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(
@@ -441,6 +458,8 @@ async def test_no_ww_light(
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
@@ -449,7 +468,7 @@ async def test_no_ww_light(
"turn_on",
{
"entity_id": "light.led_bulb_6_multi_colour_level",
- "white_value": white_color,
+ "rgbw_color": [0, 0, 0, white_color],
},
blocking=True,
)
@@ -472,7 +491,8 @@ async def test_no_ww_light(
state = hass.states.get("light.led_bulb_6_multi_colour_level")
assert state is not None
assert state.state == "on"
- assert state.attributes["white_value"] == 190
+ assert state.attributes["color_mode"] == "rgbw"
+ assert state.attributes["rgbw_color"] == (0, 0, 0, 190)
async def test_no_cw_light(
@@ -485,6 +505,8 @@ async def test_no_cw_light(
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
@@ -493,7 +515,7 @@ async def test_no_cw_light(
"turn_on",
{
"entity_id": "light.led_bulb_6_multi_colour_level",
- "white_value": white_color,
+ "rgbw_color": [0, 0, 0, white_color],
},
blocking=True,
)
@@ -516,7 +538,8 @@ async def test_no_cw_light(
state = hass.states.get("light.led_bulb_6_multi_colour_level")
assert state is not None
assert state.state == "on"
- assert state.attributes["white_value"] == 190
+ 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):
@@ -527,6 +550,8 @@ async def test_wc_light(hass, light_wc_data, light_msg, light_rgb_msg, sent_mess
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
@@ -559,6 +584,7 @@ async def test_wc_light(hass, light_wc_data, light_msg, light_rgb_msg, sent_mess
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):
@@ -569,6 +595,8 @@ async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages)
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
@@ -597,6 +625,8 @@ async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages)
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(
@@ -649,3 +679,5 @@ async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages)
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/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py
index 4841cd5a940..f3ab44844a2 100644
--- a/tests/components/philips_js/test_config_flow.py
+++ b/tests/components/philips_js/test_config_flow.py
@@ -4,8 +4,8 @@ from unittest.mock import ANY, patch
from haphilipsjs import PairingFailure
from pytest import fixture
-from homeassistant import config_entries
-from homeassistant.components.philips_js.const import DOMAIN
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components.philips_js.const import CONF_ALLOW_NOTIFY, DOMAIN
from . import (
MOCK_CONFIG,
@@ -17,13 +17,17 @@ from . import (
MOCK_USERNAME,
)
+from tests.common import MockConfigEntry
-@fixture(autouse=True)
-def mock_setup_entry():
+
+@fixture(autouse=True, name="mock_setup_entry")
+def mock_setup_entry_fixture():
"""Disable component setup."""
with patch(
"homeassistant.components.philips_js.async_setup_entry", return_value=True
- ) as mock_setup_entry:
+ ) as mock_setup_entry, patch(
+ "homeassistant.components.philips_js.async_unload_entry", return_value=True
+ ):
yield mock_setup_entry
@@ -226,3 +230,28 @@ async def test_pair_grant_failed(hass, mock_tv_pairable, mock_setup_entry):
"reason": "pairing_failure",
"type": "abort",
}
+
+
+async def test_options_flow(hass):
+ """Test config flow options."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="123456",
+ data=MOCK_CONFIG_PAIRED,
+ )
+ 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"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={CONF_ALLOW_NOTIFY: True}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert config_entry.options == {CONF_ALLOW_NOTIFY: True}
diff --git a/tests/components/ping/test_init.py b/tests/components/ping/test_init.py
deleted file mode 100644
index 3dfe193c4d5..00000000000
--- a/tests/components/ping/test_init.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""Test ping id allocation."""
-
-from homeassistant.components.ping import async_get_next_ping_id
-from homeassistant.components.ping.const import (
- DEFAULT_START_ID,
- DOMAIN,
- MAX_PING_ID,
- PING_ID,
-)
-
-
-async def test_async_get_next_ping_id(hass):
- """Verify we allocate ping ids as expected."""
- hass.data[DOMAIN] = {PING_ID: DEFAULT_START_ID}
-
- assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 1
- assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 2
- assert async_get_next_ping_id(hass, 2) == DEFAULT_START_ID + 3
- assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 5
-
- hass.data[DOMAIN][PING_ID] = MAX_PING_ID
- assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 1
- assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 2
-
- hass.data[DOMAIN][PING_ID] = MAX_PING_ID
- assert async_get_next_ping_id(hass, 2) == DEFAULT_START_ID + 1
- assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 3
diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py
index d9f02c49341..4892262fc32 100644
--- a/tests/components/plex/test_browse_media.py
+++ b/tests/components/plex/test_browse_media.py
@@ -11,7 +11,12 @@ from .const import DEFAULT_DATA
async def test_browse_media(
- hass, hass_ws_client, mock_plex_server, requests_mock, library_movies_filtertypes
+ hass,
+ hass_ws_client,
+ mock_plex_server,
+ requests_mock,
+ library_movies_filtertypes,
+ empty_payload,
):
"""Test getting Plex clients from plex.tv."""
websocket_client = await hass_ws_client(hass)
@@ -92,6 +97,10 @@ async def test_browse_media(
f"{mock_plex_server.url_in_use}/library/sections/1/all?includeMeta=1",
text=library_movies_filtertypes,
)
+ requests_mock.get(
+ f"{mock_plex_server.url_in_use}/library/sections/1/collections?includeMeta=1",
+ text=empty_payload,
+ )
msg_id += 1
library_section_id = next(iter(mock_plex_server.library.sections())).key
diff --git a/tests/components/pvpc_hourly_pricing/conftest.py b/tests/components/pvpc_hourly_pricing/conftest.py
index a16923d0a73..2421c753518 100644
--- a/tests/components/pvpc_hourly_pricing/conftest.py
+++ b/tests/components/pvpc_hourly_pricing/conftest.py
@@ -14,6 +14,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker
FIXTURE_JSON_DATA_2019_10_26 = "PVPC_CURV_DD_2019_10_26.json"
FIXTURE_JSON_DATA_2019_10_27 = "PVPC_CURV_DD_2019_10_27.json"
FIXTURE_JSON_DATA_2019_10_29 = "PVPC_CURV_DD_2019_10_29.json"
+FIXTURE_JSON_DATA_2021_06_01 = "PVPC_CURV_DD_2021_06_01.json"
def check_valid_state(state, tariff: str, value=None, key_attr=None):
@@ -60,4 +61,10 @@ def pvpc_aioclient_mock(aioclient_mock: AiohttpClientMocker):
text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2019_10_29}"),
)
+ # new format for prices >= 2021-06-01
+ aioclient_mock.get(
+ "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2021-06-01",
+ text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2021_06_01}"),
+ )
+
return aioclient_mock
diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py
index 2a64d81ef98..1c9cb7e133d 100644
--- a/tests/components/pvpc_hourly_pricing/test_config_flow.py
+++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py
@@ -3,7 +3,13 @@ from datetime import datetime
from unittest.mock import patch
from homeassistant import config_entries, data_entry_flow
-from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN
+from homeassistant.components.pvpc_hourly_pricing import (
+ ATTR_POWER,
+ ATTR_POWER_P3,
+ ATTR_TARIFF,
+ DOMAIN,
+ TARIFFS,
+)
from homeassistant.const import CONF_NAME
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
@@ -20,13 +26,20 @@ async def test_config_flow(
"""
Test config flow for pvpc_hourly_pricing.
- - Create a new entry with tariff "normal"
+ - Create a new entry with tariff "2.0TD (Ceuta/Melilla)"
- Check state and attributes
- Check abort when trying to config another with same tariff
- Check removal and add again to check state restoration
+ - Configure options to change power and tariff to "2.0TD"
"""
hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid")
- mock_data = {"return_time": datetime(2019, 10, 26, 14, 0, tzinfo=date_util.UTC)}
+ tst_config = {
+ CONF_NAME: "test",
+ ATTR_TARIFF: TARIFFS[1],
+ ATTR_POWER: 4.6,
+ ATTR_POWER_P3: 5.75,
+ }
+ mock_data = {"return_time": datetime(2021, 6, 1, 12, 0, tzinfo=date_util.UTC)}
def mock_now():
return mock_data["return_time"]
@@ -38,13 +51,13 @@ async def test_config_flow(
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(
- result["flow_id"], {CONF_NAME: "test", ATTR_TARIFF: "normal"}
+ result["flow_id"], tst_config
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
await hass.async_block_till_done()
state = hass.states.get("sensor.test")
- check_valid_state(state, tariff="normal")
+ check_valid_state(state, tariff=TARIFFS[1])
assert pvpc_aioclient_mock.call_count == 1
# Check abort when configuring another with same tariff
@@ -53,7 +66,7 @@ async def test_config_flow(
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(
- result["flow_id"], {CONF_NAME: "test", ATTR_TARIFF: "normal"}
+ result["flow_id"], tst_config
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert pvpc_aioclient_mock.call_count == 1
@@ -70,11 +83,38 @@ async def test_config_flow(
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(
- result["flow_id"], {CONF_NAME: "test", ATTR_TARIFF: "normal"}
+ result["flow_id"], tst_config
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
await hass.async_block_till_done()
state = hass.states.get("sensor.test")
- check_valid_state(state, tariff="normal")
+ check_valid_state(state, tariff=TARIFFS[1])
+ price_pbc = state.state
assert pvpc_aioclient_mock.call_count == 2
+ assert state.attributes["period"] == "P2"
+ assert state.attributes["next_period"] == "P1"
+ assert state.attributes["available_power"] == 4600
+
+ # check options flow
+ current_entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(current_entries) == 1
+ config_entry = current_entries[0]
+
+ 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"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={ATTR_TARIFF: TARIFFS[0], ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6},
+ )
+ await hass.async_block_till_done()
+ state = hass.states.get("sensor.test")
+ price_cym = state.state
+ check_valid_state(state, tariff=TARIFFS[0])
+ assert pvpc_aioclient_mock.call_count == 3
+ assert state.attributes["period"] == "P2"
+ assert state.attributes["next_period"] == "P1"
+ assert state.attributes["available_power"] == 3000
+ assert price_cym < price_pbc
diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py
index 19f3a7aa31c..cee2374f192 100644
--- a/tests/components/pvpc_hourly_pricing/test_sensor.py
+++ b/tests/components/pvpc_hourly_pricing/test_sensor.py
@@ -3,15 +3,20 @@ from datetime import datetime, timedelta
import logging
from unittest.mock import patch
-from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN
+from homeassistant.components.pvpc_hourly_pricing import (
+ ATTR_POWER,
+ ATTR_POWER_P3,
+ ATTR_TARIFF,
+ DOMAIN,
+ TARIFFS,
+)
from homeassistant.const import CONF_NAME
from homeassistant.core import ATTR_NOW, EVENT_TIME_CHANGED
-from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from .conftest import check_valid_state
-from tests.common import date_util
+from tests.common import MockConfigEntry, date_util, mock_registry
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -32,14 +37,27 @@ async def test_sensor_availability(
):
"""Test sensor availability and handling of cloud access."""
hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid")
- config = {DOMAIN: [{CONF_NAME: "test_dst", ATTR_TARIFF: "discrimination"}]}
+ config_entry = MockConfigEntry(
+ domain=DOMAIN, data={CONF_NAME: "test_dst", ATTR_TARIFF: "discrimination"}
+ )
+ config_entry.add_to_hass(hass)
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
mock_data = {"return_time": datetime(2019, 10, 27, 20, 0, 0, tzinfo=date_util.UTC)}
def mock_now():
return mock_data["return_time"]
with patch("homeassistant.util.dt.utcnow", new=mock_now):
- assert await async_setup_component(hass, DOMAIN, config)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+
+ # check migration
+ current_entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(current_entries) == 1
+ migrated_entry = current_entries[0]
+ assert migrated_entry.version == 1
+ assert migrated_entry.data[ATTR_POWER] == migrated_entry.data[ATTR_POWER_P3]
+ assert migrated_entry.data[ATTR_TARIFF] == TARIFFS[0]
+
await hass.async_block_till_done()
caplog.clear()
assert pvpc_aioclient_mock.call_count == 2
@@ -85,3 +103,64 @@ async def test_sensor_availability(
assert pvpc_aioclient_mock.call_count == 33
assert len(caplog.messages) == 1
assert caplog.records[0].levelno == logging.WARNING
+
+
+async def test_multi_sensor_migration(
+ hass, caplog, legacy_patchable_time, pvpc_aioclient_mock: AiohttpClientMocker
+):
+ """Test tariff migration when there are >1 old sensors."""
+ entity_reg = mock_registry(hass)
+ hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid")
+ uid_1 = "discrimination"
+ uid_2 = "normal"
+ old_conf_1 = {CONF_NAME: "test_pvpc_1", ATTR_TARIFF: uid_1}
+ old_conf_2 = {CONF_NAME: "test_pvpc_2", ATTR_TARIFF: uid_2}
+
+ config_entry_1 = MockConfigEntry(domain=DOMAIN, data=old_conf_1, unique_id=uid_1)
+ config_entry_1.add_to_hass(hass)
+ entity1 = entity_reg.async_get_or_create(
+ domain="sensor",
+ platform=DOMAIN,
+ unique_id=uid_1,
+ config_entry=config_entry_1,
+ suggested_object_id="test_pvpc_1",
+ )
+
+ config_entry_2 = MockConfigEntry(domain=DOMAIN, data=old_conf_2, unique_id=uid_2)
+ config_entry_2.add_to_hass(hass)
+ entity2 = entity_reg.async_get_or_create(
+ domain="sensor",
+ platform=DOMAIN,
+ unique_id=uid_2,
+ config_entry=config_entry_2,
+ suggested_object_id="test_pvpc_2",
+ )
+
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 2
+ assert len(entity_reg.entities) == 2
+
+ mock_data = {"return_time": datetime(2019, 10, 27, 20, tzinfo=date_util.UTC)}
+
+ def mock_now():
+ return mock_data["return_time"]
+
+ caplog.clear()
+ with caplog.at_level(logging.WARNING):
+ with patch("homeassistant.util.dt.utcnow", new=mock_now):
+ assert await hass.config_entries.async_setup(config_entry_1.entry_id)
+ assert len(caplog.messages) == 2
+
+ # check migration with removal of extra sensors
+ assert len(entity_reg.entities) == 1
+ assert entity1.entity_id in entity_reg.entities
+ assert entity2.entity_id not in entity_reg.entities
+
+ current_entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(current_entries) == 1
+ migrated_entry = current_entries[0]
+ assert migrated_entry.version == 1
+ assert migrated_entry.data[ATTR_POWER] == migrated_entry.data[ATTR_POWER_P3]
+ assert migrated_entry.data[ATTR_TARIFF] == TARIFFS[0]
+
+ await hass.async_block_till_done()
+ assert pvpc_aioclient_mock.call_count == 2
diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py
index 6727b4da495..40ad71096c1 100644
--- a/tests/components/recorder/test_purge.py
+++ b/tests/components/recorder/test_purge.py
@@ -8,6 +8,8 @@ from sqlalchemy.exc import DatabaseError, OperationalError
from sqlalchemy.orm.session import Session
from homeassistant.components import recorder
+from homeassistant.components.recorder import PurgeTask
+from homeassistant.components.recorder.const import MAX_ROWS_TO_PURGE
from homeassistant.components.recorder.models import Events, RecorderRuns, States
from homeassistant.components.recorder.purge import purge_old_data
from homeassistant.components.recorder.util import session_scope
@@ -43,8 +45,10 @@ async def test_purge_old_states(
events = session.query(Events).filter(Events.event_type == "state_changed")
assert events.count() == 6
+ purge_before = dt_util.utcnow() - timedelta(days=4)
+
# run purge_old_data()
- finished = purge_old_data(instance, 4, repack=False)
+ finished = purge_old_data(instance, purge_before, repack=False)
assert not finished
assert states.count() == 2
@@ -52,7 +56,7 @@ async def test_purge_old_states(
assert states_after_purge[1].old_state_id == states_after_purge[0].state_id
assert states_after_purge[0].old_state_id is None
- finished = purge_old_data(instance, 4, repack=False)
+ finished = purge_old_data(instance, purge_before, repack=False)
assert finished
assert states.count() == 2
@@ -162,13 +166,15 @@ async def test_purge_old_events(
events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%"))
assert events.count() == 6
+ purge_before = dt_util.utcnow() - timedelta(days=4)
+
# run purge_old_data()
- finished = purge_old_data(instance, 4, repack=False)
+ finished = purge_old_data(instance, purge_before, repack=False)
assert not finished
assert events.count() == 2
# we should only have 2 events left
- finished = purge_old_data(instance, 4, repack=False)
+ finished = purge_old_data(instance, purge_before, repack=False)
assert finished
assert events.count() == 2
@@ -186,11 +192,13 @@ async def test_purge_old_recorder_runs(
recorder_runs = session.query(RecorderRuns)
assert recorder_runs.count() == 7
+ purge_before = dt_util.utcnow()
+
# run purge_old_data()
- finished = purge_old_data(instance, 0, repack=False)
+ finished = purge_old_data(instance, purge_before, repack=False)
assert not finished
- finished = purge_old_data(instance, 0, repack=False)
+ finished = purge_old_data(instance, purge_before, repack=False)
assert finished
assert recorder_runs.count() == 1
@@ -322,6 +330,94 @@ async def test_purge_edge_case(
assert events.count() == 0
+async def test_purge_cutoff_date(
+ hass: HomeAssistant,
+ async_setup_recorder_instance: SetupRecorderInstanceT,
+):
+ """Test states and events are purged only if they occurred before "now() - keep_days"."""
+
+ async def _add_db_entries(hass: HomeAssistant, cutoff: datetime, rows: int) -> None:
+ timestamp_keep = cutoff
+ timestamp_purge = cutoff - timedelta(microseconds=1)
+
+ with recorder.session_scope(hass=hass) as session:
+ session.add(
+ Events(
+ event_id=1000,
+ event_type="KEEP",
+ event_data="{}",
+ origin="LOCAL",
+ created=timestamp_keep,
+ time_fired=timestamp_keep,
+ )
+ )
+ session.add(
+ States(
+ entity_id="test.cutoff",
+ domain="sensor",
+ state="keep",
+ attributes="{}",
+ last_changed=timestamp_keep,
+ last_updated=timestamp_keep,
+ created=timestamp_keep,
+ event_id=1000,
+ )
+ )
+ for row in range(1, rows):
+ session.add(
+ Events(
+ event_id=1000 + row,
+ event_type="PURGE",
+ event_data="{}",
+ origin="LOCAL",
+ created=timestamp_purge,
+ time_fired=timestamp_purge,
+ )
+ )
+ session.add(
+ States(
+ entity_id="test.cutoff",
+ domain="sensor",
+ state="purge",
+ attributes="{}",
+ last_changed=timestamp_purge,
+ last_updated=timestamp_purge,
+ created=timestamp_purge,
+ event_id=1000 + row,
+ )
+ )
+
+ instance = await async_setup_recorder_instance(hass, None)
+ await async_wait_purge_done(hass, instance)
+
+ service_data = {"keep_days": 2}
+
+ # Force multiple purge batches to be run
+ rows = MAX_ROWS_TO_PURGE + 1
+ cutoff = dt_util.utcnow() - timedelta(days=service_data["keep_days"])
+ await _add_db_entries(hass, cutoff, rows)
+
+ with session_scope(hass=hass) as session:
+ states = session.query(States)
+ events = session.query(Events)
+ assert states.filter(States.state == "purge").count() == rows - 1
+ assert states.filter(States.state == "keep").count() == 1
+ assert events.filter(Events.event_type == "PURGE").count() == rows - 1
+ assert events.filter(Events.event_type == "KEEP").count() == 1
+
+ instance.queue.put(PurgeTask(cutoff, repack=False, apply_filter=False))
+ await hass.async_block_till_done()
+ await async_recorder_block_till_done(hass, instance)
+ await async_wait_purge_done(hass, instance)
+
+ states = session.query(States)
+ events = session.query(Events)
+ assert states.filter(States.state == "purge").count() == 0
+ assert states.filter(States.state == "keep").count() == 1
+ assert events.filter(Events.event_type == "PURGE").count() == 0
+ assert events.filter(Events.event_type == "KEEP").count() == 1
+
+
async def test_purge_filtered_states(
hass: HomeAssistant,
async_setup_recorder_instance: SetupRecorderInstanceT,
diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py
index 1ec0f2284b4..32eaaaab842 100644
--- a/tests/components/recorder/test_statistics.py
+++ b/tests/components/recorder/test_statistics.py
@@ -8,7 +8,11 @@ from pytest import approx
from homeassistant.components.recorder import history
from homeassistant.components.recorder.const import DATA_INSTANCE
from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat
-from homeassistant.components.recorder.statistics import statistics_during_period
+from homeassistant.components.recorder.statistics import (
+ get_last_statistics,
+ statistics_during_period,
+)
+from homeassistant.const import TEMP_CELSIUS
from homeassistant.setup import setup_component
import homeassistant.util.dt as dt_util
@@ -24,23 +28,69 @@ def test_compile_hourly_statistics(hass_recorder):
hist = history.get_significant_states(hass, zero, four)
assert dict(states) == dict(hist)
+ for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}):
+ stats = statistics_during_period(hass, zero, **kwargs)
+ assert stats == {}
+ stats = get_last_statistics(hass, 0, "sensor.test1")
+ assert stats == {}
+
recorder.do_adhoc_statistics(period="hourly", start=zero)
+ recorder.do_adhoc_statistics(period="hourly", start=four)
wait_recording_done(hass)
- stats = statistics_during_period(hass, zero)
- assert stats == {
- "sensor.test1": [
- {
- "statistic_id": "sensor.test1",
- "start": process_timestamp_to_utc_isoformat(zero),
- "mean": approx(14.915254237288135),
- "min": approx(10.0),
- "max": approx(20.0),
- "last_reset": None,
- "state": None,
- "sum": None,
- }
- ]
+ expected_1 = {
+ "statistic_id": "sensor.test1",
+ "start": process_timestamp_to_utc_isoformat(zero),
+ "mean": approx(14.915254237288135),
+ "min": approx(10.0),
+ "max": approx(20.0),
+ "last_reset": None,
+ "state": None,
+ "sum": None,
}
+ expected_2 = {
+ "statistic_id": "sensor.test1",
+ "start": process_timestamp_to_utc_isoformat(four),
+ "mean": approx(20.0),
+ "min": approx(20.0),
+ "max": approx(20.0),
+ "last_reset": None,
+ "state": None,
+ "sum": None,
+ }
+ expected_stats1 = [
+ {**expected_1, "statistic_id": "sensor.test1"},
+ {**expected_2, "statistic_id": "sensor.test1"},
+ ]
+ expected_stats2 = [
+ {**expected_1, "statistic_id": "sensor.test2"},
+ {**expected_2, "statistic_id": "sensor.test2"},
+ ]
+
+ # Test statistics_during_period
+ stats = statistics_during_period(hass, zero)
+ assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2}
+
+ stats = statistics_during_period(hass, zero, statistic_ids=["sensor.test2"])
+ assert stats == {"sensor.test2": expected_stats2}
+
+ stats = statistics_during_period(hass, zero, statistic_ids=["sensor.test3"])
+ assert stats == {}
+
+ # Test get_last_statistics
+ stats = get_last_statistics(hass, 0, "sensor.test1")
+ assert stats == {}
+
+ stats = get_last_statistics(hass, 1, "sensor.test1")
+ assert stats == {"sensor.test1": [{**expected_2, "statistic_id": "sensor.test1"}]}
+
+ stats = get_last_statistics(hass, 2, "sensor.test1")
+ assert stats == {"sensor.test1": expected_stats1[::-1]}
+
+ stats = get_last_statistics(hass, 3, "sensor.test1")
+ assert stats == {"sensor.test1": expected_stats1[::-1]}
+
+ stats = get_last_statistics(hass, 1, "sensor.test3")
+ assert stats == {}
def record_states(hass):
@@ -52,9 +102,19 @@ def record_states(hass):
sns1 = "sensor.test1"
sns2 = "sensor.test2"
sns3 = "sensor.test3"
- sns1_attr = {"device_class": "temperature", "state_class": "measurement"}
- sns2_attr = {"device_class": "temperature"}
- sns3_attr = {}
+ sns4 = "sensor.test4"
+ sns1_attr = {
+ "device_class": "temperature",
+ "state_class": "measurement",
+ "unit_of_measurement": TEMP_CELSIUS,
+ }
+ sns2_attr = {
+ "device_class": "humidity",
+ "state_class": "measurement",
+ "unit_of_measurement": "%",
+ }
+ sns3_attr = {"device_class": "temperature"}
+ sns4_attr = {}
def set_state(entity_id, state, **kwargs):
"""Set the state."""
@@ -68,7 +128,7 @@ def record_states(hass):
three = two + timedelta(minutes=30)
four = three + timedelta(minutes=15)
- states = {mp: [], sns1: [], sns2: [], sns3: []}
+ states = {mp: [], sns1: [], sns2: [], sns3: [], sns4: []}
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one):
states[mp].append(
set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)})
@@ -79,15 +139,18 @@ def record_states(hass):
states[sns1].append(set_state(sns1, "10", attributes=sns1_attr))
states[sns2].append(set_state(sns2, "10", attributes=sns2_attr))
states[sns3].append(set_state(sns3, "10", attributes=sns3_attr))
+ states[sns4].append(set_state(sns4, "10", attributes=sns4_attr))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two):
states[sns1].append(set_state(sns1, "15", attributes=sns1_attr))
states[sns2].append(set_state(sns2, "15", attributes=sns2_attr))
states[sns3].append(set_state(sns3, "15", attributes=sns3_attr))
+ states[sns4].append(set_state(sns4, "15", attributes=sns4_attr))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three):
states[sns1].append(set_state(sns1, "20", attributes=sns1_attr))
states[sns2].append(set_state(sns2, "20", attributes=sns2_attr))
states[sns3].append(set_state(sns3, "20", attributes=sns3_attr))
+ states[sns4].append(set_state(sns4, "20", attributes=sns4_attr))
return zero, four, states
diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py
index 278c6d7f18a..c3da2652a6d 100644
--- a/tests/components/samsungtv/conftest.py
+++ b/tests/components/samsungtv/conftest.py
@@ -21,6 +21,7 @@ def remote_fixture():
remote = Mock()
remote.__enter__ = Mock()
remote.__exit__ = Mock()
+ remote.port.return_value = 55000
remote_class.return_value = remote
yield remote
@@ -37,6 +38,7 @@ def remotews_fixture():
remotews = Mock()
remotews.__enter__ = Mock()
remotews.__exit__ = Mock()
+ remotews.port.return_value = 8002
remotews.rest_device_info.return_value = {
"id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4",
"device": {
diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py
index 1dd11fa5ad9..a0d2875ca59 100644
--- a/tests/components/samsungtv/test_config_flow.py
+++ b/tests/components/samsungtv/test_config_flow.py
@@ -14,6 +14,7 @@ from homeassistant.components.samsungtv.const import (
CONF_MODEL,
DEFAULT_MANUFACTURER,
DOMAIN,
+ LEGACY_PORT,
METHOD_LEGACY,
METHOD_WEBSOCKET,
RESULT_AUTH_MISSING,
@@ -362,6 +363,29 @@ async def test_ssdp_legacy_not_supported(hass: HomeAssistant, remote: Mock):
assert result["reason"] == RESULT_NOT_SUPPORTED
+async def test_ssdp_websocket_success_populates_mac_address(
+ hass: HomeAssistant, remotews: Mock
+):
+ """Test starting a flow from ssdp for a supported device populates the mac."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "confirm"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input="whatever"
+ )
+ assert result["type"] == "create_entry"
+ assert result["title"] == "Living Room (82GXARRS)"
+ assert result["data"][CONF_HOST] == "fake_host"
+ assert result["data"][CONF_NAME] == "Living Room"
+ assert result["data"][CONF_MAC] == "aa:bb:cc:dd:ee:ff"
+ assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer"
+ assert result["data"][CONF_MODEL] == "82GXARRS"
+ assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de"
+
+
async def test_ssdp_websocket_not_supported(hass: HomeAssistant, remote: Mock):
"""Test starting a flow from discovery for not supported device."""
with patch(
@@ -491,7 +515,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant, remote: Mock):
assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de"
-async def test_import_legacy(hass: HomeAssistant):
+async def test_import_legacy(hass: HomeAssistant, remote: Mock):
"""Test importing from yaml with hostname."""
with patch(
"homeassistant.components.samsungtv.config_flow.socket.gethostbyname",
@@ -505,14 +529,18 @@ async def test_import_legacy(hass: HomeAssistant):
await hass.async_block_till_done()
assert result["type"] == "create_entry"
assert result["title"] == "fake"
- assert result["data"][CONF_METHOD] == METHOD_LEGACY
assert result["data"][CONF_HOST] == "fake_host"
assert result["data"][CONF_NAME] == "fake"
assert result["data"][CONF_MANUFACTURER] == "Samsung"
assert result["result"].unique_id is None
+ entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(entries) == 1
+ assert entries[0].data[CONF_METHOD] == METHOD_LEGACY
+ assert entries[0].data[CONF_PORT] == LEGACY_PORT
-async def test_import_legacy_without_name(hass: HomeAssistant):
+
+async def test_import_legacy_without_name(hass: HomeAssistant, remote: Mock):
"""Test importing from yaml without a name."""
with patch(
"homeassistant.components.samsungtv.config_flow.socket.gethostbyname",
@@ -526,11 +554,15 @@ async def test_import_legacy_without_name(hass: HomeAssistant):
await hass.async_block_till_done()
assert result["type"] == "create_entry"
assert result["title"] == "fake_host"
- assert result["data"][CONF_METHOD] == METHOD_LEGACY
assert result["data"][CONF_HOST] == "fake_host"
assert result["data"][CONF_MANUFACTURER] == "Samsung"
assert result["result"].unique_id is None
+ entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(entries) == 1
+ assert entries[0].data[CONF_METHOD] == METHOD_LEGACY
+ assert entries[0].data[CONF_PORT] == LEGACY_PORT
+
async def test_import_websocket(hass: HomeAssistant):
"""Test importing from yaml with hostname."""
@@ -547,12 +579,38 @@ async def test_import_websocket(hass: HomeAssistant):
assert result["type"] == "create_entry"
assert result["title"] == "fake"
assert result["data"][CONF_METHOD] == METHOD_WEBSOCKET
+ assert result["data"][CONF_PORT] == 8002
assert result["data"][CONF_HOST] == "fake_host"
assert result["data"][CONF_NAME] == "fake"
assert result["data"][CONF_MANUFACTURER] == "Samsung"
assert result["result"].unique_id is None
+async def test_import_websocket_without_port(hass: HomeAssistant, remotews: Mock):
+ """Test importing from yaml with hostname by no port."""
+ with patch(
+ "homeassistant.components.samsungtv.config_flow.socket.gethostbyname",
+ return_value="fake_host",
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=MOCK_IMPORT_WSDATA,
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == "create_entry"
+ assert result["title"] == "fake"
+ assert result["data"][CONF_HOST] == "fake_host"
+ assert result["data"][CONF_NAME] == "fake"
+ assert result["data"][CONF_MANUFACTURER] == "Samsung"
+ assert result["result"].unique_id is None
+
+ entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(entries) == 1
+ assert entries[0].data[CONF_METHOD] == METHOD_WEBSOCKET
+ assert entries[0].data[CONF_PORT] == 8002
+
+
async def test_import_unknown_host(hass: HomeAssistant, remotews: Mock):
"""Test importing from yaml with hostname that does not resolve."""
with patch(
@@ -687,6 +745,7 @@ async def test_autodetect_websocket(hass: HomeAssistant, remote: Mock, remotews:
"id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4",
"device": {
"modelName": "82GXARRS",
+ "networkType": "wireless",
"wifiMac": "aa:bb:cc:dd:ee:ff",
"udn": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4",
"mac": "aa:bb:cc:dd:ee:ff",
@@ -707,6 +766,11 @@ async def test_autodetect_websocket(hass: HomeAssistant, remote: Mock, remotews:
call(**AUTODETECT_WEBSOCKET_SSL),
call(**DEVICEINFO_WEBSOCKET_SSL),
]
+ await hass.async_block_till_done()
+
+ entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(entries) == 1
+ assert entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff"
async def test_autodetect_auth_missing(hass: HomeAssistant, remote: Mock):
@@ -747,14 +811,14 @@ async def test_autodetect_not_supported(hass: HomeAssistant, remote: Mock):
async def test_autodetect_legacy(hass: HomeAssistant, remote: Mock):
"""Test for send key with autodetection of protocol."""
- with patch("homeassistant.components.samsungtv.bridge.Remote") as remote:
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA
- )
- assert result["type"] == "create_entry"
- assert result["data"][CONF_METHOD] == "legacy"
- assert remote.call_count == 1
- assert remote.call_args_list == [call(AUTODETECT_LEGACY)]
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"][CONF_METHOD] == "legacy"
+ assert result["data"][CONF_NAME] == "fake_name"
+ assert result["data"][CONF_MAC] is None
+ assert result["data"][CONF_PORT] == LEGACY_PORT
async def test_autodetect_none(hass: HomeAssistant, remote: Mock, remotews: Mock):
@@ -830,12 +894,22 @@ async def test_update_missing_mac_unique_id_added_from_dhcp(hass, remotews: Mock
"""Test missing mac and unique id added."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None)
entry.add_to_hass(hass)
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": config_entries.SOURCE_DHCP},
- data=MOCK_DHCP_DATA,
- )
- await hass.async_block_till_done()
+ with patch(
+ "homeassistant.components.samsungtv.async_setup",
+ return_value=True,
+ ) as mock_setup, patch(
+ "homeassistant.components.samsungtv.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_DHCP},
+ data=MOCK_DHCP_DATA,
+ )
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff"
@@ -846,18 +920,53 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf(hass, remotews:
"""Test missing mac and unique id added."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None)
entry.add_to_hass(hass)
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": config_entries.SOURCE_ZEROCONF},
- data=MOCK_ZEROCONF_DATA,
- )
- await hass.async_block_till_done()
+ with patch(
+ "homeassistant.components.samsungtv.async_setup",
+ return_value=True,
+ ) as mock_setup, patch(
+ "homeassistant.components.samsungtv.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ data=MOCK_ZEROCONF_DATA,
+ )
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff"
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
+async def test_update_missing_mac_unique_id_added_from_ssdp(hass, remotews: Mock):
+ """Test missing mac and unique id added via ssdp."""
+ entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None)
+ entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.samsungtv.async_setup",
+ return_value=True,
+ ) as mock_setup, patch(
+ "homeassistant.components.samsungtv.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_SSDP},
+ data=MOCK_SSDP_DATA,
+ )
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+ assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff"
+ assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de"
+
+
async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf(
hass, remotews: Mock
):
@@ -868,12 +977,21 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf(
unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de",
)
entry.add_to_hass(hass)
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": config_entries.SOURCE_ZEROCONF},
- data=MOCK_ZEROCONF_DATA,
- )
- await hass.async_block_till_done()
+ with patch(
+ "homeassistant.components.samsungtv.async_setup",
+ return_value=True,
+ ) as mock_setup, patch(
+ "homeassistant.components.samsungtv.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ data=MOCK_ZEROCONF_DATA,
+ )
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff"
diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py
index f728fd4af10..c5c1519556d 100644
--- a/tests/components/samsungtv/test_init.py
+++ b/tests/components/samsungtv/test_init.py
@@ -8,10 +8,12 @@ from homeassistant.components.samsungtv.const import (
METHOD_WEBSOCKET,
)
from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV
+from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
CONF_HOST,
+ CONF_MAC,
CONF_METHOD,
CONF_NAME,
SERVICE_VOLUME_UP,
@@ -30,6 +32,16 @@ MOCK_CONFIG = {
}
]
}
+MOCK_CONFIG_WITHOUT_PORT = {
+ SAMSUNGTV_DOMAIN: [
+ {
+ CONF_HOST: "fake_host",
+ CONF_NAME: "fake",
+ CONF_ON_ACTION: [{"delay": "00:00:01"}],
+ }
+ ]
+}
+
REMOTE_CALL = {
"name": "HomeAssistant",
"description": "HomeAssistant",
@@ -67,6 +79,41 @@ async def test_setup(hass: HomeAssistant, remote: Mock):
assert remote.call_args == call(REMOTE_CALL)
+async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant):
+ """Test import from yaml when the device is offline."""
+ with patch(
+ "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError
+ ), patch(
+ "homeassistant.components.samsungtv.bridge.SamsungTVWS.open",
+ side_effect=OSError,
+ ), patch(
+ "homeassistant.components.samsungtv.config_flow.socket.gethostbyname",
+ return_value="fake_host",
+ ):
+ await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG)
+ await hass.async_block_till_done()
+
+ config_entries_domain = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN)
+ assert len(config_entries_domain) == 1
+ assert config_entries_domain[0].state == ConfigEntryState.SETUP_RETRY
+
+
+async def test_setup_from_yaml_without_port_device_online(
+ hass: HomeAssistant, remotews: Mock
+):
+ """Test import from yaml when the device is online."""
+ with patch(
+ "homeassistant.components.samsungtv.config_flow.socket.gethostbyname",
+ return_value="fake_host",
+ ):
+ await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG)
+ await hass.async_block_till_done()
+
+ config_entries_domain = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN)
+ assert len(config_entries_domain) == 1
+ assert config_entries_domain[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff"
+
+
async def test_setup_duplicate_config(hass: HomeAssistant, remote: Mock, caplog):
"""Test duplicate setup of platform."""
DUPLICATE = {
diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py
index 2cdd5cf56df..de9183915a2 100644
--- a/tests/components/samsungtv/test_media_player.py
+++ b/tests/components/samsungtv/test_media_player.py
@@ -159,14 +159,33 @@ async def test_setup_websocket(hass, remotews, mock_now):
remote = Mock()
remote.__enter__ = Mock(return_value=enter)
remote.__exit__ = Mock()
+ remote.rest_device_info.return_value = {
+ "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4",
+ "device": {
+ "modelName": "82GXARRS",
+ "wifiMac": "aa:bb:cc:dd:ee:ff",
+ "name": "[TV] Living Room",
+ "type": "Samsung SmartTV",
+ "networkType": "wireless",
+ },
+ }
remote_class.return_value = remote
await setup_samsungtv(hass, MOCK_CONFIGWS)
- assert remote_class.call_count == 1
- assert remote_class.call_args_list == [call(**MOCK_CALLS_WS)]
+ assert remote_class.call_count == 2
+ assert remote_class.call_args_list == [
+ call(**MOCK_CALLS_WS),
+ call(**MOCK_CALLS_WS),
+ ]
assert hass.states.get(ENTITY_ID)
+ await hass.async_block_till_done()
+
+ config_entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN)
+ assert len(config_entries) == 1
+ assert config_entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff"
+
async def test_setup_websocket_2(hass, mock_now):
"""Test setup of platform from config entry."""
@@ -183,20 +202,37 @@ async def test_setup_websocket_2(hass, mock_now):
assert len(config_entries) == 1
assert entry is config_entries[0]
- assert await async_setup_component(hass, SAMSUNGTV_DOMAIN, {})
- await hass.async_block_till_done()
-
- next_update = mock_now + timedelta(minutes=5)
- with patch(
- "homeassistant.components.samsungtv.bridge.SamsungTVWS"
- ) as remote, patch("homeassistant.util.dt.utcnow", return_value=next_update):
- async_fire_time_changed(hass, next_update)
+ with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class:
+ enter = Mock()
+ type(enter).token = PropertyMock(return_value="987654321")
+ remote = Mock()
+ remote.__enter__ = Mock(return_value=enter)
+ remote.__exit__ = Mock()
+ remote.rest_device_info.return_value = {
+ "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4",
+ "device": {
+ "modelName": "82GXARRS",
+ "wifiMac": "aa:bb:cc:dd:ee:ff",
+ "name": "[TV] Living Room",
+ "type": "Samsung SmartTV",
+ "networkType": "wireless",
+ },
+ }
+ remote_class.return_value = remote
+ assert await async_setup_component(hass, SAMSUNGTV_DOMAIN, {})
await hass.async_block_till_done()
+ assert config_entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff"
+
+ next_update = mock_now + timedelta(minutes=5)
+ with patch("homeassistant.util.dt.utcnow", return_value=next_update):
+ async_fire_time_changed(hass, next_update)
+ await hass.async_block_till_done()
+
state = hass.states.get(entity_id)
assert state
- assert remote.call_count == 1
- assert remote.call_args_list == [call(**MOCK_CALLS_WS)]
+ assert remote_class.call_count == 3
+ assert remote_class.call_args_list[0] == call(**MOCK_CALLS_WS)
async def test_update_on(hass, remote, mock_now):
@@ -304,6 +340,32 @@ async def test_update_unhandled_response(hass, remote, mock_now):
assert state.state == STATE_ON
+async def test_connection_closed_during_update_can_recover(hass, remote, mock_now):
+ """Testing update tv connection closed exception can recover."""
+ await setup_samsungtv(hass, MOCK_CONFIG)
+
+ with patch(
+ "homeassistant.components.samsungtv.bridge.Remote",
+ side_effect=[exceptions.ConnectionClosed(), DEFAULT_MOCK],
+ ):
+
+ next_update = mock_now + timedelta(minutes=5)
+ with patch("homeassistant.util.dt.utcnow", return_value=next_update):
+ async_fire_time_changed(hass, next_update)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == STATE_OFF
+
+ next_update = mock_now + timedelta(minutes=10)
+ with patch("homeassistant.util.dt.utcnow", return_value=next_update):
+ async_fire_time_changed(hass, next_update)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == STATE_ON
+
+
async def test_send_key(hass, remote):
"""Test for send key."""
await setup_samsungtv(hass, MOCK_CONFIG)
diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py
index 82935f2b41f..e9d320aa9ef 100644
--- a/tests/components/search/test_init.py
+++ b/tests/components/search/test_init.py
@@ -3,6 +3,7 @@ from homeassistant.components import search
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
+ entity,
entity_registry as er,
)
from homeassistant.setup import async_setup_component
@@ -10,6 +11,18 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
+MOCK_ENTITY_SOURCES = {
+ "light.platform_config_source": {
+ "source": entity.SOURCE_PLATFORM_CONFIG,
+ "domain": "wled",
+ },
+ "light.config_entry_source": {
+ "source": entity.SOURCE_CONFIG_ENTRY,
+ "config_entry": "config_entry_id",
+ "domain": "wled",
+ },
+}
+
async def test_search(hass):
"""Test that search works."""
@@ -48,6 +61,18 @@ async def test_search(hass):
device_id=wled_device.id,
)
+ entity_sources = {
+ "light.wled_platform_config_source": {
+ "source": entity.SOURCE_PLATFORM_CONFIG,
+ "domain": "wled",
+ },
+ "light.wled_config_entry_source": {
+ "source": entity.SOURCE_CONFIG_ENTRY,
+ "config_entry": wled_config_entry.entry_id,
+ "domain": "wled",
+ },
+ }
+
# Non related info.
kitchen_area = area_reg.async_create("Kitchen")
@@ -221,7 +246,7 @@ async def test_search(hass):
("automation", "automation.wled_entity"),
("automation", "automation.wled_device"),
):
- searcher = search.Searcher(hass, device_reg, entity_reg)
+ searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources)
results = searcher.async_search(search_type, search_id)
# Add the item we searched for, it's omitted from results
results.setdefault(search_type, set()).add(search_id)
@@ -254,7 +279,7 @@ async def test_search(hass):
("scene", "scene.scene_wled_hue"),
("group", "group.wled_hue"),
):
- searcher = search.Searcher(hass, device_reg, entity_reg)
+ searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources)
results = searcher.async_search(search_type, search_id)
# Add the item we searched for, it's omitted from results
results.setdefault(search_type, set()).add(search_id)
@@ -276,9 +301,14 @@ async def test_search(hass):
("script", "script.non_existing"),
("automation", "automation.non_existing"),
):
- searcher = search.Searcher(hass, device_reg, entity_reg)
+ searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources)
assert searcher.async_search(search_type, search_id) == {}
+ searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources)
+ assert searcher.async_search("entity", "light.wled_config_entry_source") == {
+ "config_entry": {wled_config_entry.entry_id},
+ }
+
async def test_area_lookup(hass):
"""Test area based lookup."""
@@ -326,13 +356,13 @@ async def test_area_lookup(hass):
},
)
- searcher = search.Searcher(hass, device_reg, entity_reg)
+ searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES)
assert searcher.async_search("area", living_room_area.id) == {
"script": {"script.wled"},
"automation": {"automation.area_turn_on"},
}
- searcher = search.Searcher(hass, device_reg, entity_reg)
+ searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES)
assert searcher.async_search("automation", "automation.area_turn_on") == {
"area": {living_room_area.id},
}
diff --git a/tests/components/select/__init__.py b/tests/components/select/__init__.py
new file mode 100644
index 00000000000..1e7aecea908
--- /dev/null
+++ b/tests/components/select/__init__.py
@@ -0,0 +1 @@
+"""The tests for the Select integration."""
diff --git a/tests/components/select/test_device_action.py b/tests/components/select/test_device_action.py
new file mode 100644
index 00000000000..5c2486a4e26
--- /dev/null
+++ b/tests/components/select/test_device_action.py
@@ -0,0 +1,138 @@
+"""The tests for Select device actions."""
+import pytest
+import voluptuous_serialize
+
+from homeassistant.components import automation
+from homeassistant.components.select import DOMAIN
+from homeassistant.components.select.device_action import async_get_action_capabilities
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import (
+ config_validation as cv,
+ device_registry,
+ entity_registry,
+)
+from homeassistant.setup import async_setup_component
+
+from tests.common import (
+ MockConfigEntry,
+ assert_lists_same,
+ async_get_device_automations,
+ async_mock_service,
+ mock_device_registry,
+ mock_registry,
+)
+
+
+@pytest.fixture
+def device_reg(hass: HomeAssistant) -> device_registry.DeviceRegistry:
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass: HomeAssistant) -> entity_registry.EntityRegistry:
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+async def test_get_actions(
+ hass: HomeAssistant,
+ device_reg: device_registry.DeviceRegistry,
+ entity_reg: entity_registry.EntityRegistry,
+) -> None:
+ """Test we get the expected actions from a select."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_actions = [
+ {
+ "domain": DOMAIN,
+ "type": "select_option",
+ "device_id": device_entry.id,
+ "entity_id": "select.test_5678",
+ }
+ ]
+ actions = await async_get_device_automations(hass, "action", device_entry.id)
+ assert_lists_same(actions, expected_actions)
+
+
+async def test_action(hass: HomeAssistant) -> None:
+ """Test for select_option action."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "event",
+ "event_type": "test_event",
+ },
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "abcdefgh",
+ "entity_id": "select.entity",
+ "type": "select_option",
+ "option": "option1",
+ },
+ },
+ ]
+ },
+ )
+
+ select_calls = async_mock_service(hass, DOMAIN, "select_option")
+
+ hass.bus.async_fire("test_event")
+ await hass.async_block_till_done()
+ assert len(select_calls) == 1
+ assert select_calls[0].domain == DOMAIN
+ assert select_calls[0].service == "select_option"
+ assert select_calls[0].data == {"entity_id": "select.entity", "option": "option1"}
+
+
+async def test_get_action_capabilities(hass: HomeAssistant) -> None:
+ """Test we get the expected capabilities from a select action."""
+ config = {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": "select_option",
+ "entity_id": "select.test",
+ "option": "option1",
+ }
+
+ # Test when entity doesn't exists
+ capabilities = await async_get_action_capabilities(hass, config)
+ assert capabilities
+ assert "extra_fields" in capabilities
+ assert voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ ) == [
+ {
+ "name": "option",
+ "required": True,
+ "type": "select",
+ "options": [],
+ },
+ ]
+
+ # Mock an entity
+ hass.states.async_set("select.test", "option1", {"options": ["option1", "option2"]})
+
+ # Test if we get the right capabilities now
+ capabilities = await async_get_action_capabilities(hass, config)
+ assert capabilities
+ assert "extra_fields" in capabilities
+ assert voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ ) == [
+ {
+ "name": "option",
+ "required": True,
+ "type": "select",
+ "options": [("option1", "option1"), ("option2", "option2")],
+ },
+ ]
diff --git a/tests/components/select/test_device_condition.py b/tests/components/select/test_device_condition.py
new file mode 100644
index 00000000000..d5ee88156cf
--- /dev/null
+++ b/tests/components/select/test_device_condition.py
@@ -0,0 +1,201 @@
+"""The tests for Select device conditions."""
+from __future__ import annotations
+
+import pytest
+import voluptuous_serialize
+
+from homeassistant.components import automation
+from homeassistant.components.select import DOMAIN
+from homeassistant.components.select.device_condition import (
+ async_get_condition_capabilities,
+)
+from homeassistant.core import HomeAssistant, ServiceCall
+from homeassistant.helpers import (
+ config_validation as cv,
+ device_registry,
+ entity_registry,
+)
+from homeassistant.setup import async_setup_component
+
+from tests.common import (
+ MockConfigEntry,
+ assert_lists_same,
+ async_get_device_automations,
+ async_mock_service,
+ mock_device_registry,
+ mock_registry,
+)
+
+
+@pytest.fixture
+def device_reg(hass: HomeAssistant) -> device_registry.DeviceRegistry:
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass: HomeAssistant) -> entity_registry.EntityRegistry:
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+@pytest.fixture
+def calls(hass: HomeAssistant) -> list[ServiceCall]:
+ """Track calls to a mock service."""
+ return async_mock_service(hass, "test", "automation")
+
+
+async def test_get_conditions(
+ hass: HomeAssistant,
+ device_reg: device_registry.DeviceRegistry,
+ entity_reg: entity_registry.EntityRegistry,
+) -> None:
+ """Test we get the expected conditions from a select."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_conditions = [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": "selected_option",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ }
+ ]
+ conditions = await async_get_device_automations(hass, "condition", device_entry.id)
+ assert_lists_same(conditions, expected_conditions)
+
+
+async def test_if_selected_option(
+ hass: HomeAssistant, calls: list[ServiceCall]
+) -> None:
+ """Test for selected_option conditions."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": "select.entity",
+ "type": "selected_option",
+ "option": "option1",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data": {
+ "result": "option1 - {{ trigger.platform }} - {{ trigger.event.event_type }}"
+ },
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event2"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": "select.entity",
+ "type": "selected_option",
+ "option": "option2",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data": {
+ "result": "option2 - {{ trigger.platform }} - {{ trigger.event.event_type }}"
+ },
+ },
+ },
+ ]
+ },
+ )
+
+ # Test with non existing entity
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ hass.states.async_set(
+ "select.entity", "option1", {"options": ["option1", "option2"]}
+ )
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["result"] == "option1 - event - test_event1"
+
+ hass.states.async_set(
+ "select.entity", "option2", {"options": ["option1", "option2"]}
+ )
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["result"] == "option2 - event - test_event2"
+
+
+async def test_get_condition_capabilities(hass: HomeAssistant) -> None:
+ """Test we get the expected capabilities from a select condition."""
+ config = {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": "selected_option",
+ "entity_id": "select.test",
+ "option": "option1",
+ }
+
+ # Test when entity doesn't exists
+ capabilities = await async_get_condition_capabilities(hass, config)
+ assert capabilities
+ assert "extra_fields" in capabilities
+ assert voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ ) == [
+ {
+ "name": "option",
+ "required": True,
+ "type": "select",
+ "options": [],
+ },
+ {
+ "name": "for",
+ "optional": True,
+ "type": "positive_time_period_dict",
+ },
+ ]
+
+ # Mock an entity
+ hass.states.async_set("select.test", "option1", {"options": ["option1", "option2"]})
+
+ # Test if we get the right capabilities now
+ capabilities = await async_get_condition_capabilities(hass, config)
+ assert capabilities
+ assert "extra_fields" in capabilities
+ assert voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ ) == [
+ {
+ "name": "option",
+ "required": True,
+ "type": "select",
+ "options": [("option1", "option1"), ("option2", "option2")],
+ },
+ {
+ "name": "for",
+ "optional": True,
+ "type": "positive_time_period_dict",
+ },
+ ]
diff --git a/tests/components/select/test_device_trigger.py b/tests/components/select/test_device_trigger.py
new file mode 100644
index 00000000000..b0066e9ac22
--- /dev/null
+++ b/tests/components/select/test_device_trigger.py
@@ -0,0 +1,240 @@
+"""The tests for Select device triggers."""
+from __future__ import annotations
+
+import pytest
+import voluptuous_serialize
+
+from homeassistant.components import automation
+from homeassistant.components.select import DOMAIN
+from homeassistant.components.select.device_trigger import (
+ async_get_trigger_capabilities,
+)
+from homeassistant.core import HomeAssistant, ServiceCall
+from homeassistant.helpers import config_validation as cv, device_registry
+from homeassistant.helpers.entity_registry import EntityRegistry
+from homeassistant.setup import async_setup_component
+
+from tests.common import (
+ MockConfigEntry,
+ assert_lists_same,
+ async_get_device_automations,
+ async_mock_service,
+ mock_device_registry,
+ mock_registry,
+)
+
+
+@pytest.fixture
+def device_reg(hass: HomeAssistant) -> device_registry.DeviceRegistry:
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass: HomeAssistant) -> EntityRegistry:
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+@pytest.fixture
+def calls(hass: HomeAssistant) -> list[ServiceCall]:
+ """Track calls to a mock service."""
+ return async_mock_service(hass, "test", "automation")
+
+
+async def test_get_triggers(
+ hass: HomeAssistant,
+ device_reg: device_registry.DeviceRegistry,
+ entity_reg: EntityRegistry,
+) -> None:
+ """Test we get the expected triggers from a select."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_triggers = [
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": "current_option_changed",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ }
+ ]
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert_lists_same(triggers, expected_triggers)
+
+
+async def test_if_fires_on_state_change(hass, calls):
+ """Test for turn_on and turn_off triggers firing."""
+ hass.states.async_set(
+ "select.entity", "option1", {"options": ["option1", "option2", "option3"]}
+ )
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": "select.entity",
+ "type": "current_option_changed",
+ "to": "option2",
+ },
+ "action": {
+ "service": "test.automation",
+ "data": {
+ "some": (
+ "to - {{ trigger.platform}} - "
+ "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - "
+ "{{ trigger.to_state.state}} - {{ trigger.for }} - "
+ "{{ trigger.id}}"
+ )
+ },
+ },
+ },
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": "select.entity",
+ "type": "current_option_changed",
+ "from": "option2",
+ },
+ "action": {
+ "service": "test.automation",
+ "data": {
+ "some": (
+ "from - {{ trigger.platform}} - "
+ "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - "
+ "{{ trigger.to_state.state}} - {{ trigger.for }} - "
+ "{{ trigger.id}}"
+ )
+ },
+ },
+ },
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": "select.entity",
+ "type": "current_option_changed",
+ "from": "option3",
+ "to": "option1",
+ },
+ "action": {
+ "service": "test.automation",
+ "data": {
+ "some": (
+ "from-to - {{ trigger.platform}} - "
+ "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - "
+ "{{ trigger.to_state.state}} - {{ trigger.for }} - "
+ "{{ trigger.id}}"
+ )
+ },
+ },
+ },
+ ]
+ },
+ )
+
+ # Test triggering device trigger with a to state
+ hass.states.async_set("select.entity", "option2")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data[
+ "some"
+ ] == "to - device - {} - option1 - option2 - None - 0".format("select.entity")
+
+ # Test triggering device trigger with a from state
+ hass.states.async_set("select.entity", "option3")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data[
+ "some"
+ ] == "from - device - {} - option2 - option3 - None - 0".format("select.entity")
+
+ # Test triggering device trigger with both a from and to state
+ hass.states.async_set("select.entity", "option1")
+ await hass.async_block_till_done()
+ assert len(calls) == 3
+ assert calls[2].data[
+ "some"
+ ] == "from-to - device - {} - option3 - option1 - None - 0".format("select.entity")
+
+
+async def test_get_trigger_capabilities(hass: HomeAssistant) -> None:
+ """Test we get the expected capabilities from a select trigger."""
+ config = {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": "current_option_changed",
+ "entity_id": "select.test",
+ "to": "option1",
+ }
+
+ # Test when entity doesn't exists
+ capabilities = await async_get_trigger_capabilities(hass, config)
+ assert capabilities
+ assert "extra_fields" in capabilities
+ assert voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ ) == [
+ {
+ "name": "from",
+ "optional": True,
+ "type": "select",
+ "options": [],
+ },
+ {
+ "name": "to",
+ "optional": True,
+ "type": "select",
+ "options": [],
+ },
+ {
+ "name": "for",
+ "optional": True,
+ "type": "positive_time_period_dict",
+ "optional": True,
+ },
+ ]
+
+ # Mock an entity
+ hass.states.async_set("select.test", "option1", {"options": ["option1", "option2"]})
+
+ # Test if we get the right capabilities now
+ capabilities = await async_get_trigger_capabilities(hass, config)
+ assert capabilities
+ assert "extra_fields" in capabilities
+ assert voluptuous_serialize.convert(
+ capabilities["extra_fields"], custom_serializer=cv.custom_serializer
+ ) == [
+ {
+ "name": "from",
+ "optional": True,
+ "type": "select",
+ "options": [("option1", "option1"), ("option2", "option2")],
+ },
+ {
+ "name": "to",
+ "optional": True,
+ "type": "select",
+ "options": [("option1", "option1"), ("option2", "option2")],
+ },
+ {
+ "name": "for",
+ "optional": True,
+ "type": "positive_time_period_dict",
+ "optional": True,
+ },
+ ]
diff --git a/tests/components/select/test_init.py b/tests/components/select/test_init.py
new file mode 100644
index 00000000000..188099164c2
--- /dev/null
+++ b/tests/components/select/test_init.py
@@ -0,0 +1,28 @@
+"""The tests for the Select component."""
+from homeassistant.components.select import SelectEntity
+from homeassistant.core import HomeAssistant
+
+
+class MockSelectEntity(SelectEntity):
+ """Mock SelectEntity to use in tests."""
+
+ _attr_current_option = "option_one"
+ _attr_options = ["option_one", "option_two", "option_three"]
+
+
+async def test_select(hass: HomeAssistant) -> None:
+ """Test getting data from the mocked select entity."""
+ select = MockSelectEntity()
+ assert select.current_option == "option_one"
+ assert select.state == "option_one"
+ assert select.options == ["option_one", "option_two", "option_three"]
+
+ # Test none selected
+ select._attr_current_option = None
+ assert select.current_option is None
+ assert select.state is None
+
+ # Test none existing selected
+ select._attr_current_option = "option_four"
+ assert select.current_option == "option_four"
+ assert select.state is None
diff --git a/tests/components/select/test_reproduce_state.py b/tests/components/select/test_reproduce_state.py
new file mode 100644
index 00000000000..b1ab3a0a5aa
--- /dev/null
+++ b/tests/components/select/test_reproduce_state.py
@@ -0,0 +1,57 @@
+"""Test reproduce state for select entities."""
+import pytest
+
+from homeassistant.components.select.const import (
+ ATTR_OPTION,
+ ATTR_OPTIONS,
+ DOMAIN,
+ SERVICE_SELECT_OPTION,
+)
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import HomeAssistant, State
+
+from tests.common import async_mock_service
+
+
+async def test_reproducing_states(
+ hass: HomeAssistant, caplog: pytest.LogCaptureFixture
+) -> None:
+ """Test reproducing select states."""
+ calls = async_mock_service(hass, DOMAIN, SERVICE_SELECT_OPTION)
+ hass.states.async_set(
+ "select.test",
+ "option_one",
+ {ATTR_OPTIONS: ["option_one", "option_two", "option_three"]},
+ )
+
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("select.test", "option_two"),
+ ],
+ )
+
+ assert len(calls) == 1
+ assert calls[0].domain == DOMAIN
+ assert calls[0].data == {ATTR_ENTITY_ID: "select.test", ATTR_OPTION: "option_two"}
+
+ # Calling it again should not do anything
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("select.test", "option_one"),
+ ],
+ )
+ assert len(calls) == 1
+
+ # Restoring an invalid state should not work either
+ await hass.helpers.state.async_reproduce_state(
+ [State("select.test", "option_four")]
+ )
+ assert len(calls) == 1
+ assert "Invalid state specified" in caplog.text
+
+ # Restoring an state for an invalid entity ID logs a warning
+ await hass.helpers.state.async_reproduce_state(
+ [State("select.non_existing", "option_three")]
+ )
+ assert len(calls) == 1
+ assert "Unable to find entity" in caplog.text
diff --git a/tests/components/select/test_significant_change.py b/tests/components/select/test_significant_change.py
new file mode 100644
index 00000000000..34ae5cad54e
--- /dev/null
+++ b/tests/components/select/test_significant_change.py
@@ -0,0 +1,20 @@
+"""Test the select significant change platform."""
+from homeassistant.components.select.significant_change import (
+ async_check_significant_change,
+)
+from homeassistant.core import HomeAssistant
+
+
+async def test_significant_change(hass: HomeAssistant) -> None:
+ """Detect select significant change."""
+ attrs1 = {"options": ["option1", "option2"]}
+ attrs2 = {"options": ["option1", "option2", "option3"]}
+
+ assert not async_check_significant_change(
+ hass, "option1", attrs1, "option1", attrs1
+ )
+ assert not async_check_significant_change(
+ hass, "option1", attrs1, "option1", attrs2
+ )
+ assert async_check_significant_change(hass, "option1", attrs1, "option2", attrs1)
+ assert async_check_significant_change(hass, "option1", attrs1, "option2", attrs2)
diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py
index 6cad21c5bde..daf452cf715 100644
--- a/tests/components/sensor/test_device_condition.py
+++ b/tests/components/sensor/test_device_condition.py
@@ -4,7 +4,12 @@ import pytest
import homeassistant.components.automation as automation
from homeassistant.components.sensor import DOMAIN
from homeassistant.components.sensor.device_condition import ENTITY_CONDITIONS
-from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN
+from homeassistant.const import (
+ CONF_PLATFORM,
+ DEVICE_CLASS_BATTERY,
+ PERCENTAGE,
+ STATE_UNKNOWN,
+)
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
@@ -80,8 +85,60 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr
assert conditions == expected_conditions
+async def test_get_conditions_no_state(hass, device_reg, entity_reg):
+ """Test we get the expected conditions from a sensor."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_ids = {}
+ for device_class in DEVICE_CLASSES:
+ entity_ids[device_class] = entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ f"5678_{device_class}",
+ device_id=device_entry.id,
+ device_class=device_class,
+ unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class),
+ ).entity_id
+
+ await hass.async_block_till_done()
+
+ expected_conditions = [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": condition["type"],
+ "device_id": device_entry.id,
+ "entity_id": entity_ids[device_class],
+ }
+ for device_class in DEVICE_CLASSES
+ if device_class in UNITS_OF_MEASUREMENT
+ for condition in ENTITY_CONDITIONS[device_class]
+ if device_class != "none"
+ ]
+ conditions = await async_get_device_automations(hass, "condition", device_entry.id)
+ assert conditions == expected_conditions
+
+
+@pytest.mark.parametrize(
+ "set_state,device_class_reg,device_class_state,unit_reg,unit_state",
+ [
+ (False, DEVICE_CLASS_BATTERY, None, PERCENTAGE, None),
+ (True, None, DEVICE_CLASS_BATTERY, None, PERCENTAGE),
+ ],
+)
async def test_get_condition_capabilities(
- hass, device_reg, entity_reg, enable_custom_integrations
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ device_class_reg,
+ device_class_state,
+ unit_reg,
+ unit_state,
):
"""Test we get the expected capabilities from a sensor condition."""
platform = getattr(hass.components, f"test.{DOMAIN}")
@@ -93,15 +150,20 @@ async def test_get_condition_capabilities(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
- entity_reg.async_get_or_create(
+ entity_id = entity_reg.async_get_or_create(
DOMAIN,
"test",
platform.ENTITIES["battery"].unique_id,
device_id=device_entry.id,
- )
-
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
- await hass.async_block_till_done()
+ device_class=device_class_reg,
+ unit_of_measurement=unit_reg,
+ ).entity_id
+ if set_state:
+ hass.states.async_set(
+ entity_id,
+ None,
+ {"device_class": device_class_state, "unit_of_measurement": unit_state},
+ )
expected_capabilities = {
"extra_fields": [
diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py
index 9da93510523..ce35e2506a9 100644
--- a/tests/components/sensor/test_device_trigger.py
+++ b/tests/components/sensor/test_device_trigger.py
@@ -6,7 +6,12 @@ import pytest
import homeassistant.components.automation as automation
from homeassistant.components.sensor import DOMAIN
from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS
-from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN
+from homeassistant.const import (
+ CONF_PLATFORM,
+ DEVICE_CLASS_BATTERY,
+ PERCENTAGE,
+ STATE_UNKNOWN,
+)
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -85,8 +90,22 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat
assert triggers == expected_triggers
+@pytest.mark.parametrize(
+ "set_state,device_class_reg,device_class_state,unit_reg,unit_state",
+ [
+ (False, DEVICE_CLASS_BATTERY, None, PERCENTAGE, None),
+ (True, None, DEVICE_CLASS_BATTERY, None, PERCENTAGE),
+ ],
+)
async def test_get_trigger_capabilities(
- hass, device_reg, entity_reg, enable_custom_integrations
+ hass,
+ device_reg,
+ entity_reg,
+ set_state,
+ device_class_reg,
+ device_class_state,
+ unit_reg,
+ unit_state,
):
"""Test we get the expected capabilities from a sensor trigger."""
platform = getattr(hass.components, f"test.{DOMAIN}")
@@ -98,15 +117,20 @@ async def test_get_trigger_capabilities(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
- entity_reg.async_get_or_create(
+ entity_id = entity_reg.async_get_or_create(
DOMAIN,
"test",
platform.ENTITIES["battery"].unique_id,
device_id=device_entry.id,
- )
-
- assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
- await hass.async_block_till_done()
+ device_class=device_class_reg,
+ unit_of_measurement=unit_reg,
+ ).entity_id
+ if set_state:
+ hass.states.async_set(
+ entity_id,
+ None,
+ {"device_class": device_class_state, "unit_of_measurement": unit_state},
+ )
expected_capabilities = {
"extra_fields": [
diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py
index 47a950f9eaa..99ede396381 100644
--- a/tests/components/sensor/test_recorder.py
+++ b/tests/components/sensor/test_recorder.py
@@ -1,32 +1,141 @@
"""The tests for sensor recorder platform."""
# pylint: disable=protected-access,invalid-name
from datetime import timedelta
-from unittest.mock import patch, sentinel
+from unittest.mock import patch
+import pytest
from pytest import approx
from homeassistant.components.recorder import history
from homeassistant.components.recorder.const import DATA_INSTANCE
from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat
-from homeassistant.components.recorder.statistics import statistics_during_period
+from homeassistant.components.recorder.statistics import (
+ list_statistic_ids,
+ statistics_during_period,
+)
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.setup import setup_component
import homeassistant.util.dt as dt_util
from tests.components.recorder.common import wait_recording_done
+ENERGY_SENSOR_ATTRIBUTES = {
+ "device_class": "energy",
+ "state_class": "measurement",
+ "unit_of_measurement": "kWh",
+}
+POWER_SENSOR_ATTRIBUTES = {
+ "device_class": "power",
+ "state_class": "measurement",
+ "unit_of_measurement": "kW",
+}
+PRESSURE_SENSOR_ATTRIBUTES = {
+ "device_class": "pressure",
+ "state_class": "measurement",
+ "unit_of_measurement": "hPa",
+}
+TEMPERATURE_SENSOR_ATTRIBUTES = {
+ "device_class": "temperature",
+ "state_class": "measurement",
+ "unit_of_measurement": "°C",
+}
-def test_compile_hourly_statistics(hass_recorder):
+
+@pytest.mark.parametrize(
+ "device_class,unit,native_unit,mean,min,max",
+ [
+ ("battery", "%", "%", 16.440677, 10, 30),
+ ("battery", None, None, 16.440677, 10, 30),
+ ("humidity", "%", "%", 16.440677, 10, 30),
+ ("humidity", None, None, 16.440677, 10, 30),
+ ("pressure", "Pa", "Pa", 16.440677, 10, 30),
+ ("pressure", "hPa", "Pa", 1644.0677, 1000, 3000),
+ ("pressure", "mbar", "Pa", 1644.0677, 1000, 3000),
+ ("pressure", "inHg", "Pa", 55674.53, 33863.89, 101591.67),
+ ("pressure", "psi", "Pa", 113354.48, 68947.57, 206842.71),
+ ("temperature", "°C", "°C", 16.440677, 10, 30),
+ ("temperature", "°F", "°C", -8.644068, -12.22222, -1.111111),
+ ],
+)
+def test_compile_hourly_statistics(
+ hass_recorder, caplog, device_class, unit, native_unit, mean, min, max
+):
"""Test compiling hourly statistics."""
+ zero = dt_util.utcnow()
hass = hass_recorder()
recorder = hass.data[DATA_INSTANCE]
setup_component(hass, "sensor", {})
- zero, four, states = record_states(hass)
+ attributes = {
+ "device_class": device_class,
+ "state_class": "measurement",
+ "unit_of_measurement": unit,
+ }
+ four, states = record_states(hass, zero, "sensor.test1", attributes)
hist = history.get_significant_states(hass, zero, four)
assert dict(states) == dict(hist)
recorder.do_adhoc_statistics(period="hourly", start=zero)
wait_recording_done(hass)
+ statistic_ids = list_statistic_ids(hass)
+ assert statistic_ids == [
+ {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit}
+ ]
+ stats = statistics_during_period(hass, zero)
+ assert stats == {
+ "sensor.test1": [
+ {
+ "statistic_id": "sensor.test1",
+ "start": process_timestamp_to_utc_isoformat(zero),
+ "mean": approx(mean),
+ "min": approx(min),
+ "max": approx(max),
+ "last_reset": None,
+ "state": None,
+ "sum": None,
+ }
+ ]
+ }
+ assert "Error while processing event StatisticsTask" not in caplog.text
+
+
+@pytest.mark.parametrize("attributes", [TEMPERATURE_SENSOR_ATTRIBUTES])
+def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes):
+ """Test compiling hourly statistics for unsupported sensor."""
+ attributes = dict(attributes)
+ zero = dt_util.utcnow()
+ hass = hass_recorder()
+ recorder = hass.data[DATA_INSTANCE]
+ setup_component(hass, "sensor", {})
+ four, states = record_states(hass, zero, "sensor.test1", attributes)
+ if "unit_of_measurement" in attributes:
+ attributes["unit_of_measurement"] = "invalid"
+ _, _states = record_states(hass, zero, "sensor.test2", attributes)
+ states = {**states, **_states}
+ attributes.pop("unit_of_measurement")
+ _, _states = record_states(hass, zero, "sensor.test3", attributes)
+ states = {**states, **_states}
+ attributes["state_class"] = "invalid"
+ _, _states = record_states(hass, zero, "sensor.test4", attributes)
+ states = {**states, **_states}
+ attributes.pop("state_class")
+ _, _states = record_states(hass, zero, "sensor.test5", attributes)
+ states = {**states, **_states}
+ attributes["state_class"] = "measurement"
+ _, _states = record_states(hass, zero, "sensor.test6", attributes)
+ states = {**states, **_states}
+ attributes["state_class"] = "unsupported"
+ _, _states = record_states(hass, zero, "sensor.test7", attributes)
+ states = {**states, **_states}
+
+ hist = history.get_significant_states(hass, zero, four)
+ assert dict(states) == dict(hist)
+
+ recorder.do_adhoc_statistics(period="hourly", start=zero)
+ wait_recording_done(hass)
+ statistic_ids = list_statistic_ids(hass)
+ assert statistic_ids == [
+ {"statistic_id": "sensor.test1", "unit_of_measurement": "°C"}
+ ]
stats = statistics_during_period(hass, zero)
assert stats == {
"sensor.test1": [
@@ -42,19 +151,36 @@ def test_compile_hourly_statistics(hass_recorder):
}
]
}
+ assert "Error while processing event StatisticsTask" not in caplog.text
-def test_compile_hourly_energy_statistics(hass_recorder):
+@pytest.mark.parametrize(
+ "device_class,unit,native_unit,factor",
+ [
+ ("energy", "kWh", "kWh", 1),
+ ("energy", "Wh", "kWh", 1 / 1000),
+ ("monetary", "€", "€", 1),
+ ("monetary", "SEK", "SEK", 1),
+ ],
+)
+def test_compile_hourly_energy_statistics(
+ hass_recorder, caplog, device_class, unit, native_unit, factor
+):
"""Test compiling hourly statistics."""
+ zero = dt_util.utcnow()
hass = hass_recorder()
recorder = hass.data[DATA_INSTANCE]
setup_component(hass, "sensor", {})
- sns1_attr = {"device_class": "energy", "state_class": "measurement"}
- sns2_attr = {"device_class": "energy"}
- sns3_attr = {}
+ attributes = {
+ "device_class": device_class,
+ "state_class": "measurement",
+ "unit_of_measurement": unit,
+ "last_reset": None,
+ }
+ seq = [10, 15, 20, 10, 30, 40, 50, 60, 70]
- zero, four, eight, states = record_energy_states(
- hass, sns1_attr, sns2_attr, sns3_attr
+ four, eight, states = record_energy_states(
+ hass, zero, "sensor.test1", attributes, seq
)
hist = history.get_significant_states(
hass, zero - timedelta.resolution, eight + timedelta.resolution
@@ -67,6 +193,97 @@ def test_compile_hourly_energy_statistics(hass_recorder):
wait_recording_done(hass)
recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2))
wait_recording_done(hass)
+ statistic_ids = list_statistic_ids(hass)
+ assert statistic_ids == [
+ {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit}
+ ]
+ stats = statistics_during_period(hass, zero)
+ assert stats == {
+ "sensor.test1": [
+ {
+ "statistic_id": "sensor.test1",
+ "start": process_timestamp_to_utc_isoformat(zero),
+ "max": None,
+ "mean": None,
+ "min": None,
+ "last_reset": process_timestamp_to_utc_isoformat(zero),
+ "state": approx(factor * seq[2]),
+ "sum": approx(factor * 10.0),
+ },
+ {
+ "statistic_id": "sensor.test1",
+ "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)),
+ "max": None,
+ "mean": None,
+ "min": None,
+ "last_reset": process_timestamp_to_utc_isoformat(four),
+ "state": approx(factor * seq[5]),
+ "sum": approx(factor * 10.0),
+ },
+ {
+ "statistic_id": "sensor.test1",
+ "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)),
+ "max": None,
+ "mean": None,
+ "min": None,
+ "last_reset": process_timestamp_to_utc_isoformat(four),
+ "state": approx(factor * seq[8]),
+ "sum": approx(factor * 40.0),
+ },
+ ]
+ }
+ assert "Error while processing event StatisticsTask" not in caplog.text
+
+
+def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog):
+ """Test compiling hourly statistics."""
+ zero = dt_util.utcnow()
+ hass = hass_recorder()
+ recorder = hass.data[DATA_INSTANCE]
+ setup_component(hass, "sensor", {})
+ sns1_attr = {
+ "device_class": "energy",
+ "state_class": "measurement",
+ "unit_of_measurement": "kWh",
+ "last_reset": None,
+ }
+ sns2_attr = {"device_class": "energy"}
+ sns3_attr = {}
+ sns4_attr = {
+ "device_class": "energy",
+ "state_class": "measurement",
+ "unit_of_measurement": "kWh",
+ }
+ seq1 = [10, 15, 20, 10, 30, 40, 50, 60, 70]
+ seq2 = [110, 120, 130, 0, 30, 45, 55, 65, 75]
+ seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90]
+ seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90]
+
+ four, eight, states = record_energy_states(
+ hass, zero, "sensor.test1", sns1_attr, seq1
+ )
+ _, _, _states = record_energy_states(hass, zero, "sensor.test2", sns2_attr, seq2)
+ states = {**states, **_states}
+ _, _, _states = record_energy_states(hass, zero, "sensor.test3", sns3_attr, seq3)
+ states = {**states, **_states}
+ _, _, _states = record_energy_states(hass, zero, "sensor.test4", sns4_attr, seq4)
+ states = {**states, **_states}
+
+ hist = history.get_significant_states(
+ hass, zero - timedelta.resolution, eight + timedelta.resolution
+ )
+ assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"]
+
+ recorder.do_adhoc_statistics(period="hourly", start=zero)
+ wait_recording_done(hass)
+ recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1))
+ wait_recording_done(hass)
+ recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2))
+ wait_recording_done(hass)
+ statistic_ids = list_statistic_ids(hass)
+ assert statistic_ids == [
+ {"statistic_id": "sensor.test1", "unit_of_measurement": "kWh"}
+ ]
stats = statistics_during_period(hass, zero)
assert stats == {
"sensor.test1": [
@@ -102,20 +319,37 @@ def test_compile_hourly_energy_statistics(hass_recorder):
},
]
}
+ assert "Error while processing event StatisticsTask" not in caplog.text
-def test_compile_hourly_energy_statistics2(hass_recorder):
- """Test compiling hourly statistics."""
+def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog):
+ """Test compiling multiple hourly statistics."""
+ zero = dt_util.utcnow()
hass = hass_recorder()
recorder = hass.data[DATA_INSTANCE]
setup_component(hass, "sensor", {})
- sns1_attr = {"device_class": "energy", "state_class": "measurement"}
- sns2_attr = {"device_class": "energy", "state_class": "measurement"}
- sns3_attr = {"device_class": "energy", "state_class": "measurement"}
+ sns1_attr = {**ENERGY_SENSOR_ATTRIBUTES, "last_reset": None}
+ sns2_attr = {**ENERGY_SENSOR_ATTRIBUTES, "last_reset": None}
+ sns3_attr = {
+ **ENERGY_SENSOR_ATTRIBUTES,
+ "unit_of_measurement": "Wh",
+ "last_reset": None,
+ }
+ sns4_attr = {**ENERGY_SENSOR_ATTRIBUTES}
+ seq1 = [10, 15, 20, 10, 30, 40, 50, 60, 70]
+ seq2 = [110, 120, 130, 0, 30, 45, 55, 65, 75]
+ seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90]
+ seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90]
- zero, four, eight, states = record_energy_states(
- hass, sns1_attr, sns2_attr, sns3_attr
+ four, eight, states = record_energy_states(
+ hass, zero, "sensor.test1", sns1_attr, seq1
)
+ _, _, _states = record_energy_states(hass, zero, "sensor.test2", sns2_attr, seq2)
+ states = {**states, **_states}
+ _, _, _states = record_energy_states(hass, zero, "sensor.test3", sns3_attr, seq3)
+ states = {**states, **_states}
+ _, _, _states = record_energy_states(hass, zero, "sensor.test4", sns4_attr, seq4)
+ states = {**states, **_states}
hist = history.get_significant_states(
hass, zero - timedelta.resolution, eight + timedelta.resolution
)
@@ -127,6 +361,12 @@ def test_compile_hourly_energy_statistics2(hass_recorder):
wait_recording_done(hass)
recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2))
wait_recording_done(hass)
+ statistic_ids = list_statistic_ids(hass)
+ assert statistic_ids == [
+ {"statistic_id": "sensor.test1", "unit_of_measurement": "kWh"},
+ {"statistic_id": "sensor.test2", "unit_of_measurement": "kWh"},
+ {"statistic_id": "sensor.test3", "unit_of_measurement": "kWh"},
+ ]
stats = statistics_during_period(hass, zero)
assert stats == {
"sensor.test1": [
@@ -201,8 +441,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder):
"mean": None,
"min": None,
"last_reset": process_timestamp_to_utc_isoformat(zero),
- "state": approx(5.0),
- "sum": approx(5.0),
+ "state": approx(5.0 / 1000),
+ "sum": approx(5.0 / 1000),
},
{
"statistic_id": "sensor.test3",
@@ -211,8 +451,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder):
"mean": None,
"min": None,
"last_reset": process_timestamp_to_utc_isoformat(four),
- "state": approx(50.0),
- "sum": approx(30.0),
+ "state": approx(50.0 / 1000),
+ "sum": approx(30.0 / 1000),
},
{
"statistic_id": "sensor.test3",
@@ -221,19 +461,44 @@ def test_compile_hourly_energy_statistics2(hass_recorder):
"mean": None,
"min": None,
"last_reset": process_timestamp_to_utc_isoformat(four),
- "state": approx(90.0),
- "sum": approx(70.0),
+ "state": approx(90.0 / 1000),
+ "sum": approx(70.0 / 1000),
},
],
}
+ assert "Error while processing event StatisticsTask" not in caplog.text
-def test_compile_hourly_statistics_unchanged(hass_recorder):
+@pytest.mark.parametrize(
+ "device_class,unit,value",
+ [
+ ("battery", "%", 30),
+ ("battery", None, 30),
+ ("humidity", "%", 30),
+ ("humidity", None, 30),
+ ("pressure", "Pa", 30),
+ ("pressure", "hPa", 3000),
+ ("pressure", "mbar", 3000),
+ ("pressure", "inHg", 101591.67),
+ ("pressure", "psi", 206842.71),
+ ("temperature", "°C", 30),
+ ("temperature", "°F", -1.111111),
+ ],
+)
+def test_compile_hourly_statistics_unchanged(
+ hass_recorder, caplog, device_class, unit, value
+):
"""Test compiling hourly statistics, with no changes during the hour."""
+ zero = dt_util.utcnow()
hass = hass_recorder()
recorder = hass.data[DATA_INSTANCE]
setup_component(hass, "sensor", {})
- zero, four, states = record_states(hass)
+ attributes = {
+ "device_class": device_class,
+ "state_class": "measurement",
+ "unit_of_measurement": unit,
+ }
+ four, states = record_states(hass, zero, "sensor.test1", attributes)
hist = history.get_significant_states(hass, zero, four)
assert dict(states) == dict(hist)
@@ -245,23 +510,27 @@ def test_compile_hourly_statistics_unchanged(hass_recorder):
{
"statistic_id": "sensor.test1",
"start": process_timestamp_to_utc_isoformat(four),
- "mean": approx(30.0),
- "min": approx(30.0),
- "max": approx(30.0),
+ "mean": approx(value),
+ "min": approx(value),
+ "max": approx(value),
"last_reset": None,
"state": None,
"sum": None,
}
]
}
+ assert "Error while processing event StatisticsTask" not in caplog.text
-def test_compile_hourly_statistics_partially_unavailable(hass_recorder):
+def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog):
"""Test compiling hourly statistics, with the sensor being partially unavailable."""
+ zero = dt_util.utcnow()
hass = hass_recorder()
recorder = hass.data[DATA_INSTANCE]
setup_component(hass, "sensor", {})
- zero, four, states = record_states_partially_unavailable(hass)
+ four, states = record_states_partially_unavailable(
+ hass, zero, "sensor.test1", TEMPERATURE_SENSOR_ATTRIBUTES
+ )
hist = history.get_significant_states(hass, zero, four)
assert dict(states) == dict(hist)
@@ -282,35 +551,87 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder):
}
]
}
+ assert "Error while processing event StatisticsTask" not in caplog.text
-def test_compile_hourly_statistics_unavailable(hass_recorder):
+@pytest.mark.parametrize(
+ "device_class,unit,value",
+ [
+ ("battery", "%", 30),
+ ("battery", None, 30),
+ ("humidity", "%", 30),
+ ("humidity", None, 30),
+ ("pressure", "Pa", 30),
+ ("pressure", "hPa", 3000),
+ ("pressure", "mbar", 3000),
+ ("pressure", "inHg", 101591.67),
+ ("pressure", "psi", 206842.71),
+ ("temperature", "°C", 30),
+ ("temperature", "°F", -1.111111),
+ ],
+)
+def test_compile_hourly_statistics_unavailable(
+ hass_recorder, caplog, device_class, unit, value
+):
"""Test compiling hourly statistics, with the sensor being unavailable."""
+ zero = dt_util.utcnow()
hass = hass_recorder()
recorder = hass.data[DATA_INSTANCE]
setup_component(hass, "sensor", {})
- zero, four, states = record_states_partially_unavailable(hass)
+ attributes = {
+ "device_class": device_class,
+ "state_class": "measurement",
+ "unit_of_measurement": unit,
+ }
+ four, states = record_states_partially_unavailable(
+ hass, zero, "sensor.test1", attributes
+ )
+ _, _states = record_states(hass, zero, "sensor.test2", attributes)
+ states = {**states, **_states}
hist = history.get_significant_states(hass, zero, four)
assert dict(states) == dict(hist)
recorder.do_adhoc_statistics(period="hourly", start=four)
wait_recording_done(hass)
stats = statistics_during_period(hass, four)
- assert stats == {}
+ assert stats == {
+ "sensor.test2": [
+ {
+ "statistic_id": "sensor.test2",
+ "start": process_timestamp_to_utc_isoformat(four),
+ "mean": approx(value),
+ "min": approx(value),
+ "max": approx(value),
+ "last_reset": None,
+ "state": None,
+ "sum": None,
+ }
+ ]
+ }
+ assert "Error while processing event StatisticsTask" not in caplog.text
-def record_states(hass):
+def test_compile_hourly_statistics_fails(hass_recorder, caplog):
+ """Test compiling hourly statistics throws."""
+ zero = dt_util.utcnow()
+ hass = hass_recorder()
+ recorder = hass.data[DATA_INSTANCE]
+ setup_component(hass, "sensor", {})
+ with patch(
+ "homeassistant.components.sensor.recorder.compile_statistics",
+ side_effect=Exception,
+ ):
+ recorder.do_adhoc_statistics(period="hourly", start=zero)
+ wait_recording_done(hass)
+ assert "Error while processing event StatisticsTask" in caplog.text
+
+
+def record_states(hass, zero, entity_id, attributes):
"""Record some test states.
We inject a bunch of state updates for temperature sensors.
"""
- mp = "media_player.test"
- sns1 = "sensor.test1"
- sns2 = "sensor.test2"
- sns3 = "sensor.test3"
- sns1_attr = {"device_class": "temperature", "state_class": "measurement"}
- sns2_attr = {"device_class": "temperature"}
- sns3_attr = {}
+ attributes = dict(attributes)
def set_state(entity_id, state, **kwargs):
"""Set the state."""
@@ -318,46 +639,29 @@ def record_states(hass):
wait_recording_done(hass)
return hass.states.get(entity_id)
- zero = dt_util.utcnow()
one = zero + timedelta(minutes=1)
two = one + timedelta(minutes=10)
three = two + timedelta(minutes=40)
four = three + timedelta(minutes=10)
- states = {mp: [], sns1: [], sns2: [], sns3: []}
+ states = {entity_id: []}
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one):
- states[mp].append(
- set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)})
- )
- states[mp].append(
- set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)})
- )
- states[sns1].append(set_state(sns1, "10", attributes=sns1_attr))
- states[sns2].append(set_state(sns2, "10", attributes=sns2_attr))
- states[sns3].append(set_state(sns3, "10", attributes=sns3_attr))
+ states[entity_id].append(set_state(entity_id, "10", attributes=attributes))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two):
- states[sns1].append(set_state(sns1, "15", attributes=sns1_attr))
- states[sns2].append(set_state(sns2, "15", attributes=sns2_attr))
- states[sns3].append(set_state(sns3, "15", attributes=sns3_attr))
+ states[entity_id].append(set_state(entity_id, "15", attributes=attributes))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three):
- states[sns1].append(set_state(sns1, "30", attributes=sns1_attr))
- states[sns2].append(set_state(sns2, "30", attributes=sns2_attr))
- states[sns3].append(set_state(sns3, "30", attributes=sns3_attr))
+ states[entity_id].append(set_state(entity_id, "30", attributes=attributes))
- return zero, four, states
+ return four, states
-def record_energy_states(hass, _sns1_attr, _sns2_attr, _sns3_attr):
+def record_energy_states(hass, zero, entity_id, _attributes, seq):
"""Record some test states.
We inject a bunch of state updates for energy sensors.
"""
- sns1 = "sensor.test1"
- sns2 = "sensor.test2"
- sns3 = "sensor.test3"
- sns4 = "sensor.test4"
def set_state(entity_id, state, **kwargs):
"""Set the state."""
@@ -365,7 +669,6 @@ def record_energy_states(hass, _sns1_attr, _sns2_attr, _sns3_attr):
wait_recording_done(hass)
return hass.states.get(entity_id)
- zero = dt_util.utcnow()
one = zero + timedelta(minutes=15)
two = one + timedelta(minutes=30)
three = two + timedelta(minutes=15)
@@ -375,84 +678,50 @@ def record_energy_states(hass, _sns1_attr, _sns2_attr, _sns3_attr):
seven = six + timedelta(minutes=15)
eight = seven + timedelta(minutes=30)
- sns1_attr = {**_sns1_attr, "last_reset": zero.isoformat()}
- sns2_attr = {**_sns2_attr, "last_reset": zero.isoformat()}
- sns3_attr = {**_sns3_attr, "last_reset": zero.isoformat()}
- sns4_attr = {**_sns3_attr}
+ attributes = dict(_attributes)
+ if "last_reset" in _attributes:
+ attributes["last_reset"] = zero.isoformat()
- states = {sns1: [], sns2: [], sns3: [], sns4: []}
+ states = {entity_id: []}
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=zero):
- states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) # Sum 0
- states[sns2].append(set_state(sns2, "110", attributes=sns2_attr)) # Sum 0
- states[sns3].append(set_state(sns3, "0", attributes=sns3_attr)) # Sum 0
- states[sns4].append(set_state(sns4, "0", attributes=sns4_attr)) # -
+ states[entity_id].append(set_state(entity_id, seq[0], attributes=attributes))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one):
- states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) # Sum 5
- states[sns2].append(set_state(sns2, "120", attributes=sns2_attr)) # Sum 10
- states[sns3].append(set_state(sns3, "0", attributes=sns3_attr)) # Sum 0
- states[sns4].append(set_state(sns4, "0", attributes=sns4_attr)) # -
+ states[entity_id].append(set_state(entity_id, seq[1], attributes=attributes))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two):
- states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) # Sum 10
- states[sns2].append(set_state(sns2, "130", attributes=sns2_attr)) # Sum 20
- states[sns3].append(set_state(sns3, "5", attributes=sns3_attr)) # Sum 5
- states[sns4].append(set_state(sns4, "5", attributes=sns4_attr)) # -
+ states[entity_id].append(set_state(entity_id, seq[2], attributes=attributes))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three):
- states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) # Sum 0
- states[sns2].append(set_state(sns2, "0", attributes=sns2_attr)) # Sum -110
- states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) # Sum 10
- states[sns4].append(set_state(sns4, "10", attributes=sns4_attr)) # -
+ states[entity_id].append(set_state(entity_id, seq[3], attributes=attributes))
- sns1_attr = {**_sns1_attr, "last_reset": four.isoformat()}
- sns2_attr = {**_sns2_attr, "last_reset": four.isoformat()}
- sns3_attr = {**_sns3_attr, "last_reset": four.isoformat()}
+ attributes = dict(_attributes)
+ if "last_reset" in _attributes:
+ attributes["last_reset"] = four.isoformat()
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=four):
- states[sns1].append(set_state(sns1, "30", attributes=sns1_attr)) # Sum 0
- states[sns2].append(set_state(sns2, "30", attributes=sns2_attr)) # Sum -110
- states[sns3].append(set_state(sns3, "30", attributes=sns3_attr)) # Sum 10
- states[sns4].append(set_state(sns4, "30", attributes=sns4_attr)) # -
+ states[entity_id].append(set_state(entity_id, seq[4], attributes=attributes))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=five):
- states[sns1].append(set_state(sns1, "40", attributes=sns1_attr)) # Sum 10
- states[sns2].append(set_state(sns2, "45", attributes=sns2_attr)) # Sum -95
- states[sns3].append(set_state(sns3, "50", attributes=sns3_attr)) # Sum 30
- states[sns4].append(set_state(sns4, "50", attributes=sns4_attr)) # -
+ states[entity_id].append(set_state(entity_id, seq[5], attributes=attributes))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=six):
- states[sns1].append(set_state(sns1, "50", attributes=sns1_attr)) # Sum 20
- states[sns2].append(set_state(sns2, "55", attributes=sns2_attr)) # Sum -85
- states[sns3].append(set_state(sns3, "60", attributes=sns3_attr)) # Sum 40
- states[sns4].append(set_state(sns4, "60", attributes=sns4_attr)) # -
+ states[entity_id].append(set_state(entity_id, seq[6], attributes=attributes))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=seven):
- states[sns1].append(set_state(sns1, "60", attributes=sns1_attr)) # Sum 30
- states[sns2].append(set_state(sns2, "65", attributes=sns2_attr)) # Sum -75
- states[sns3].append(set_state(sns3, "80", attributes=sns3_attr)) # Sum 60
- states[sns4].append(set_state(sns4, "80", attributes=sns4_attr)) # -
+ states[entity_id].append(set_state(entity_id, seq[7], attributes=attributes))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=eight):
- states[sns1].append(set_state(sns1, "70", attributes=sns1_attr)) # Sum 40
- states[sns2].append(set_state(sns2, "75", attributes=sns2_attr)) # Sum -65
- states[sns3].append(set_state(sns3, "90", attributes=sns3_attr)) # Sum 70
+ states[entity_id].append(set_state(entity_id, seq[8], attributes=attributes))
- return zero, four, eight, states
+ return four, eight, states
-def record_states_partially_unavailable(hass):
+def record_states_partially_unavailable(hass, zero, entity_id, attributes):
"""Record some test states.
We inject a bunch of state updates temperature sensors.
"""
- mp = "media_player.test"
- sns1 = "sensor.test1"
- sns2 = "sensor.test2"
- sns3 = "sensor.test3"
- sns1_attr = {"device_class": "temperature", "state_class": "measurement"}
- sns2_attr = {"device_class": "temperature"}
- sns3_attr = {}
def set_state(entity_id, state, **kwargs):
"""Set the state."""
@@ -460,32 +729,21 @@ def record_states_partially_unavailable(hass):
wait_recording_done(hass)
return hass.states.get(entity_id)
- zero = dt_util.utcnow()
one = zero + timedelta(minutes=1)
two = one + timedelta(minutes=15)
three = two + timedelta(minutes=30)
four = three + timedelta(minutes=15)
- states = {mp: [], sns1: [], sns2: [], sns3: []}
+ states = {entity_id: []}
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one):
- states[mp].append(
- set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)})
- )
- states[mp].append(
- set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)})
- )
- states[sns1].append(set_state(sns1, "10", attributes=sns1_attr))
- states[sns2].append(set_state(sns2, "10", attributes=sns2_attr))
- states[sns3].append(set_state(sns3, "10", attributes=sns3_attr))
+ states[entity_id].append(set_state(entity_id, "10", attributes=attributes))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two):
- states[sns1].append(set_state(sns1, "25", attributes=sns1_attr))
- states[sns2].append(set_state(sns2, "25", attributes=sns2_attr))
- states[sns3].append(set_state(sns3, "25", attributes=sns3_attr))
+ states[entity_id].append(set_state(entity_id, "25", attributes=attributes))
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three):
- states[sns1].append(set_state(sns1, STATE_UNAVAILABLE, attributes=sns1_attr))
- states[sns2].append(set_state(sns2, STATE_UNAVAILABLE, attributes=sns2_attr))
- states[sns3].append(set_state(sns3, STATE_UNAVAILABLE, attributes=sns3_attr))
+ states[entity_id].append(
+ set_state(entity_id, STATE_UNAVAILABLE, attributes=attributes)
+ )
- return zero, four, states
+ return four, states
diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py
index 2246cabe33a..2876f6e3a17 100644
--- a/tests/components/sentry/test_config_flow.py
+++ b/tests/components/sentry/test_config_flow.py
@@ -87,7 +87,7 @@ async def test_user_flow_bad_dsn(hass: HomeAssistant) -> None:
assert result2.get("errors") == {"base": "bad_dsn"}
-async def test_user_flow_unkown_exception(hass: HomeAssistant) -> None:
+async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None:
"""Test we handle any unknown exception error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py
index 204518c1e5a..9679f4949e8 100644
--- a/tests/components/sia/test_config_flow.py
+++ b/tests/components/sia/test_config_flow.py
@@ -104,29 +104,6 @@ ADDITIONAL_OPTIONS = {
}
}
-BASIC_CONFIG_ENTRY = MockConfigEntry(
- domain=DOMAIN,
- data=BASE_OUT["data"],
- options=BASE_OUT["options"],
- title="SIA Alarm on port 7777",
- entry_id=BASIS_CONFIG_ENTRY_ID,
- version=1,
-)
-ADDITIONAL_CONFIG_ENTRY = MockConfigEntry(
- domain=DOMAIN,
- data=ADDITIONAL_OUT["data"],
- options=ADDITIONAL_OUT["options"],
- title="SIA Alarm on port 7777",
- entry_id=ADDITIONAL_CONFIG_ENTRY_ID,
- version=1,
-)
-
-
-@pytest.fixture(params=[False, True], ids=["user", "add_account"])
-def additional(request) -> bool:
- """Return True or False for the additional or base test."""
- return request.param
-
@pytest.fixture
async def flow_at_user_step(hass):
@@ -140,7 +117,7 @@ async def flow_at_user_step(hass):
@pytest.fixture
async def entry_with_basic_config(hass, flow_at_user_step):
"""Return a entry with a basic config."""
- with patch("pysiaalarm.aio.SIAClient.start", return_value=True):
+ with patch("homeassistant.components.sia.async_setup_entry", return_value=True):
return await hass.config_entries.flow.async_configure(
flow_at_user_step["flow_id"], BASIC_CONFIG
)
@@ -157,7 +134,7 @@ async def flow_at_add_account_step(hass, flow_at_user_step):
@pytest.fixture
async def entry_with_additional_account_config(hass, flow_at_add_account_step):
"""Return a entry with a two account config."""
- with patch("pysiaalarm.aio.SIAClient.start", return_value=True):
+ with patch("homeassistant.components.sia.async_setup_entry", return_value=True):
return await hass.config_entries.flow.async_configure(
flow_at_add_account_step["flow_id"], ADDITIONAL_ACCOUNT
)
@@ -171,20 +148,20 @@ async def setup_sia(hass, config_entry: MockConfigEntry):
await hass.async_block_till_done()
-async def test_form_start(
- hass, flow_at_user_step, flow_at_add_account_step, additional
-):
- """Start the form and check if you get the right id and schema."""
- if additional:
- assert flow_at_add_account_step["step_id"] == "add_account"
- assert flow_at_add_account_step["errors"] is None
- assert flow_at_add_account_step["data_schema"] == ACCOUNT_SCHEMA
- return
+async def test_form_start_user(hass, flow_at_user_step):
+ """Start the form and check if you get the right id and schema for the user step."""
assert flow_at_user_step["step_id"] == "user"
assert flow_at_user_step["errors"] is None
assert flow_at_user_step["data_schema"] == HUB_SCHEMA
+async def test_form_start_account(hass, flow_at_add_account_step):
+ """Start the form and check if you get the right id and schema for the additional account step."""
+ assert flow_at_add_account_step["step_id"] == "add_account"
+ assert flow_at_add_account_step["errors"] is None
+ assert flow_at_add_account_step["data_schema"] == ACCOUNT_SCHEMA
+
+
async def test_create(hass, entry_with_basic_config):
"""Test we create a entry through the form."""
assert entry_with_basic_config["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
@@ -211,9 +188,17 @@ async def test_create_additional_account(hass, entry_with_additional_account_con
assert entry_with_additional_account_config["options"] == ADDITIONAL_OUT["options"]
-async def test_abort_form(hass, entry_with_basic_config):
+async def test_abort_form(hass):
"""Test aborting a config that already exists."""
- assert entry_with_basic_config["data"][CONF_PORT] == BASIC_CONFIG[CONF_PORT]
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=BASE_OUT["data"],
+ options=BASE_OUT["options"],
+ title="SIA Alarm on port 7777",
+ entry_id=BASIS_CONFIG_ENTRY_ID,
+ version=1,
+ )
+ await setup_sia(hass, config_entry)
start_another_flow = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
@@ -235,54 +220,97 @@ async def test_abort_form(hass, entry_with_basic_config):
("zones", 0, "invalid_zones"),
],
)
-async def test_validation_errors(
+async def test_validation_errors_user(
hass,
flow_at_user_step,
- additional,
field,
value,
error,
):
- """Test we handle the different invalid inputs, both in the user and add_account flow."""
+ """Test we handle the different invalid inputs, in the user flow."""
config = BASIC_CONFIG.copy()
flow_id = flow_at_user_step["flow_id"]
- if additional:
- flow_at_add_account_step = await hass.config_entries.flow.async_configure(
- flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL
- )
- config = ADDITIONAL_ACCOUNT.copy()
- flow_id = flow_at_add_account_step["flow_id"]
-
config[field] = value
result_err = await hass.config_entries.flow.async_configure(flow_id, config)
assert result_err["type"] == "form"
assert result_err["errors"] == {"base": error}
-async def test_unknown(hass, flow_at_user_step, additional):
+@pytest.mark.parametrize(
+ "field, value, error",
+ [
+ ("encryption_key", "AAAAAAAAAAAAAZZZ", "invalid_key_format"),
+ ("encryption_key", "AAAAAAAAAAAAA", "invalid_key_length"),
+ ("account", "ZZZ", "invalid_account_format"),
+ ("account", "A", "invalid_account_length"),
+ ("ping_interval", 1500, "invalid_ping"),
+ ("zones", 0, "invalid_zones"),
+ ],
+)
+async def test_validation_errors_account(
+ hass,
+ flow_at_user_step,
+ field,
+ value,
+ error,
+):
+ """Test we handle the different invalid inputs, in the add_account flow."""
+ flow_at_add_account_step = await hass.config_entries.flow.async_configure(
+ flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL
+ )
+ config = ADDITIONAL_ACCOUNT.copy()
+ flow_id = flow_at_add_account_step["flow_id"]
+ config[field] = value
+ result_err = await hass.config_entries.flow.async_configure(flow_id, config)
+ assert result_err["type"] == "form"
+ assert result_err["errors"] == {"base": error}
+
+
+async def test_unknown_user(hass, flow_at_user_step):
"""Test unknown exceptions."""
flow_id = flow_at_user_step["flow_id"]
- if additional:
- flow_at_add_account_step = await hass.config_entries.flow.async_configure(
- flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL
- )
- flow_id = flow_at_add_account_step["flow_id"]
with patch(
"pysiaalarm.SIAAccount.validate_account",
side_effect=Exception,
):
- config = ADDITIONAL_ACCOUNT if additional else BASIC_CONFIG
+ config = BASIC_CONFIG
result_err = await hass.config_entries.flow.async_configure(flow_id, config)
assert result_err
- assert result_err["step_id"] == "add_account" if additional else "user"
+ assert result_err["step_id"] == "user"
assert result_err["errors"] == {"base": "unknown"}
- assert result_err["data_schema"] == ACCOUNT_SCHEMA if additional else HUB_SCHEMA
+ assert result_err["data_schema"] == HUB_SCHEMA
+
+
+async def test_unknown_account(hass, flow_at_user_step):
+ """Test unknown exceptions."""
+ flow_at_add_account_step = await hass.config_entries.flow.async_configure(
+ flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL
+ )
+ flow_id = flow_at_add_account_step["flow_id"]
+ with patch(
+ "pysiaalarm.SIAAccount.validate_account",
+ side_effect=Exception,
+ ):
+ config = ADDITIONAL_ACCOUNT
+ result_err = await hass.config_entries.flow.async_configure(flow_id, config)
+ assert result_err
+ assert result_err["step_id"] == "add_account"
+ assert result_err["errors"] == {"base": "unknown"}
+ assert result_err["data_schema"] == ACCOUNT_SCHEMA
async def test_options_basic(hass):
"""Test options flow for single account."""
- await setup_sia(hass, BASIC_CONFIG_ENTRY)
- result = await hass.config_entries.options.async_init(BASIC_CONFIG_ENTRY.entry_id)
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=BASE_OUT["data"],
+ options=BASE_OUT["options"],
+ title="SIA Alarm on port 7777",
+ entry_id=BASIS_CONFIG_ENTRY_ID,
+ version=1,
+ )
+ await setup_sia(hass, config_entry)
+ 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"] == "options"
assert result["last_step"]
@@ -298,10 +326,16 @@ async def test_options_basic(hass):
async def test_options_additional(hass):
"""Test options flow for single account."""
- await setup_sia(hass, ADDITIONAL_CONFIG_ENTRY)
- result = await hass.config_entries.options.async_init(
- ADDITIONAL_CONFIG_ENTRY.entry_id
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=ADDITIONAL_OUT["data"],
+ options=ADDITIONAL_OUT["options"],
+ title="SIA Alarm on port 7777",
+ entry_id=ADDITIONAL_CONFIG_ENTRY_ID,
+ version=1,
)
+ await setup_sia(hass, config_entry)
+ 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"] == "options"
assert not result["last_step"]
diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py
index a048e4b0745..4d438965806 100644
--- a/tests/components/simplisafe/test_config_flow.py
+++ b/tests/components/simplisafe/test_config_flow.py
@@ -1,5 +1,5 @@
"""Define tests for the SimpliSafe config flow."""
-from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
+from unittest.mock import AsyncMock, patch
from simplipy.errors import (
InvalidCredentialsError,
@@ -10,18 +10,11 @@ from simplipy.errors import (
from homeassistant import data_entry_flow
from homeassistant.components.simplisafe import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
-from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
+from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME
from tests.common import MockConfigEntry
-def mock_api():
- """Mock SimpliSafe API class."""
- api = MagicMock()
- type(api).refresh_token = PropertyMock(return_value="12345abc")
- return api
-
-
async def test_duplicate_error(hass):
"""Test that errors are shown when duplicates are added."""
conf = {
@@ -33,7 +26,11 @@ async def test_duplicate_error(hass):
MockConfigEntry(
domain=DOMAIN,
unique_id="user@email.com",
- data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"},
+ data={
+ CONF_USERNAME: "user@email.com",
+ CONF_PASSWORD: "password",
+ CONF_CODE: "1234",
+ },
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
@@ -49,7 +46,7 @@ async def test_invalid_credentials(hass):
conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
with patch(
- "simplipy.API.login_via_credentials",
+ "homeassistant.components.simplisafe.config_flow.get_api",
new=AsyncMock(side_effect=InvalidCredentialsError),
):
result = await hass.config_entries.flow.async_init(
@@ -102,7 +99,11 @@ async def test_step_reauth(hass):
MockConfigEntry(
domain=DOMAIN,
unique_id="user@email.com",
- data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"},
+ data={
+ CONF_USERNAME: "user@email.com",
+ CONF_PASSWORD: "password",
+ CONF_CODE: "1234",
+ },
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
@@ -118,8 +119,8 @@ async def test_step_reauth(hass):
with patch(
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
- ), patch(
- "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api())
+ ), patch("homeassistant.components.simplisafe.config_flow.get_api"), patch(
+ "homeassistant.config_entries.ConfigEntries.async_reload"
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PASSWORD: "password"}
@@ -141,7 +142,7 @@ async def test_step_user(hass):
with patch(
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
), patch(
- "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api())
+ "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock()
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
@@ -151,7 +152,7 @@ async def test_step_user(hass):
assert result["title"] == "user@email.com"
assert result["data"] == {
CONF_USERNAME: "user@email.com",
- CONF_TOKEN: "12345abc",
+ CONF_PASSWORD: "password",
CONF_CODE: "1234",
}
@@ -165,7 +166,7 @@ async def test_step_user_mfa(hass):
}
with patch(
- "simplipy.API.login_via_credentials",
+ "homeassistant.components.simplisafe.config_flow.get_api",
new=AsyncMock(side_effect=PendingAuthorizationError),
):
result = await hass.config_entries.flow.async_init(
@@ -174,7 +175,7 @@ async def test_step_user_mfa(hass):
assert result["step_id"] == "mfa"
with patch(
- "simplipy.API.login_via_credentials",
+ "homeassistant.components.simplisafe.config_flow.get_api",
new=AsyncMock(side_effect=PendingAuthorizationError),
):
# Simulate the user pressing the MFA submit button without having clicked
@@ -187,7 +188,7 @@ async def test_step_user_mfa(hass):
with patch(
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
), patch(
- "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api())
+ "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock()
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
@@ -197,7 +198,7 @@ async def test_step_user_mfa(hass):
assert result["title"] == "user@email.com"
assert result["data"] == {
CONF_USERNAME: "user@email.com",
- CONF_TOKEN: "12345abc",
+ CONF_PASSWORD: "password",
CONF_CODE: "1234",
}
@@ -207,7 +208,7 @@ async def test_unknown_error(hass):
conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
with patch(
- "simplipy.API.login_via_credentials",
+ "homeassistant.components.simplisafe.config_flow.get_api",
new=AsyncMock(side_effect=SimplipyError),
):
result = await hass.config_entries.flow.async_init(
diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py
index 0797558958e..4210772420c 100644
--- a/tests/components/sma/__init__.py
+++ b/tests/components/sma/__init__.py
@@ -69,7 +69,6 @@ MOCK_CUSTOM_SENSOR2 = {
MOCK_SETUP_DATA = dict(
{
"custom": {},
- "device_info": MOCK_DEVICE,
"sensors": [],
},
**MOCK_USER_INPUT,
@@ -91,7 +90,6 @@ MOCK_CUSTOM_SETUP_DATA = dict(
"unit": MOCK_CUSTOM_SENSOR2["unit"],
},
},
- "device_info": MOCK_DEVICE,
"sensors": [],
},
**MOCK_USER_INPUT,
diff --git a/tests/components/sma/conftest.py b/tests/components/sma/conftest.py
index 9ec9e1f5a11..80d9b38e28b 100644
--- a/tests/components/sma/conftest.py
+++ b/tests/components/sma/conftest.py
@@ -1,6 +1,9 @@
"""Fixtures for sma tests."""
from unittest.mock import patch
+from pysma.const import DEVCLASS_INVERTER
+from pysma.definitions import sensor_map
+from pysma.sensor import Sensors
import pytest
from homeassistant import config_entries
@@ -28,7 +31,9 @@ async def init_integration(hass, mock_config_entry):
"""Create a fake SMA Config Entry."""
mock_config_entry.add_to_hass(hass)
- with patch("pysma.SMA.read"):
+ with patch("pysma.SMA.read"), patch(
+ "pysma.SMA.get_sensors", return_value=Sensors(sensor_map[DEVCLASS_INVERTER])
+ ):
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/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py
index dbcecbeb43c..f262f7eeba1 100644
--- a/tests/components/sma/test_config_flow.py
+++ b/tests/components/sma/test_config_flow.py
@@ -1,7 +1,11 @@
"""Test the sma config flow."""
from unittest.mock import patch
-import aiohttp
+from pysma.exceptions import (
+ SmaAuthenticationException,
+ SmaConnectionException,
+ SmaReadException,
+)
from homeassistant import setup
from homeassistant.components.sma.const import DOMAIN
@@ -54,7 +58,7 @@ async def test_form_cannot_connect(hass):
)
with patch(
- "pysma.SMA.new_session", side_effect=aiohttp.ClientError
+ "pysma.SMA.new_session", side_effect=SmaConnectionException
), _patch_async_setup_entry() as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -73,7 +77,7 @@ async def test_form_invalid_auth(hass):
)
with patch(
- "pysma.SMA.new_session", return_value=False
+ "pysma.SMA.new_session", side_effect=SmaAuthenticationException
), _patch_async_setup_entry() as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -92,7 +96,7 @@ async def test_form_cannot_retrieve_device_info(hass):
)
with patch("pysma.SMA.new_session", return_value=True), patch(
- "pysma.SMA.read", return_value=False
+ "pysma.SMA.read", side_effect=SmaReadException
), _patch_async_setup_entry() as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py
index b86533a11df..129af154924 100644
--- a/tests/components/sma/test_sensor.py
+++ b/tests/components/sma/test_sensor.py
@@ -10,7 +10,7 @@ from . import MOCK_CUSTOM_SENSOR
async def test_sensors(hass, init_integration):
"""Test states of the sensors."""
- state = hass.states.get("sensor.current_consumption")
+ state = hass.states.get("sensor.grid_power")
assert state
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
diff --git a/tests/components/smarttub/test_binary_sensor.py b/tests/components/smarttub/test_binary_sensor.py
index 16b4f60d3e4..c84ef99328e 100644
--- a/tests/components/smarttub/test_binary_sensor.py
+++ b/tests/components/smarttub/test_binary_sensor.py
@@ -30,6 +30,7 @@ async def test_reminders(spa, setup_entry, hass):
assert state is not None
assert state.state == STATE_OFF
assert state.attributes["snoozed"] is False
+ assert state.attributes["days"] == 2
@pytest.fixture
@@ -63,7 +64,7 @@ async def test_error(spa, hass, config_entry, mock_error):
assert state.attributes["error_code"] == 11
-async def test_snooze(spa, setup_entry, hass):
+async def test_snooze_reminder(spa, setup_entry, hass):
"""Test snoozing a reminder."""
entity_id = f"binary_sensor.{spa.brand}_{spa.model}_myfilter_reminder"
@@ -75,9 +76,29 @@ async def test_snooze(spa, setup_entry, hass):
"snooze_reminder",
{
"entity_id": entity_id,
- "days": 30,
+ "days": days,
},
blocking=True,
)
reminder.snooze.assert_called_with(days)
+
+
+async def test_reset_reminder(spa, setup_entry, hass):
+ """Test snoozing a reminder."""
+
+ entity_id = f"binary_sensor.{spa.brand}_{spa.model}_myfilter_reminder"
+ reminder = spa.get_reminders.return_value[0]
+ days = 180
+
+ await hass.services.async_call(
+ "smarttub",
+ "reset_reminder",
+ {
+ "entity_id": entity_id,
+ "days": days,
+ },
+ blocking=True,
+ )
+
+ reminder.reset.assert_called_with(days)
diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py
index 46f8d0efd5f..5af1e5fcdbc 100644
--- a/tests/components/smtp/test_notify.py
+++ b/tests/components/smtp/test_notify.py
@@ -16,9 +16,9 @@ from homeassistant.setup import async_setup_component
class MockSMTP(MailNotificationService):
"""Test SMTP object that doesn't need a working server."""
- def _send_email(self, msg):
- """Just return string for testing."""
- return msg.as_string()
+ def _send_email(self, msg, recipients):
+ """Just return msg string and recipients for testing."""
+ return msg.as_string(), recipients
async def test_reload_notify(hass):
@@ -140,7 +140,7 @@ def test_send_message(message_data, data, content_type, hass, message):
"""Verify if we can send messages of all types correctly."""
sample_email = ""
with patch("email.utils.make_msgid", return_value=sample_email):
- result = message.send_message(message_data, data=data)
+ result, _ = message.send_message(message_data, data=data)
assert content_type in result
@@ -162,5 +162,30 @@ def test_send_text_message(hass, message):
sample_email = ""
message_data = "Test msg"
with patch("email.utils.make_msgid", return_value=sample_email):
- result = message.send_message(message_data)
+ result, _ = message.send_message(message_data)
assert re.search(expected, result)
+
+
+@pytest.mark.parametrize(
+ "target",
+ [
+ None,
+ "target@example.com",
+ ],
+ ids=[
+ "Verify we can send email to default recipient.",
+ "Verify email recipient can be overwritten by target arg.",
+ ],
+)
+def test_send_target_message(target, hass, message):
+ """Verify if we can send email to correct recipient."""
+ sample_email = ""
+ message_data = "Test msg"
+ with patch("email.utils.make_msgid", return_value=sample_email):
+ if not target:
+ expected_recipient = ["recip1@example.com", "testrecip@test.com"]
+ else:
+ expected_recipient = target
+
+ _, recipient = message.send_message(message_data, target=target)
+ assert recipient == expected_recipient
diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py
index b7d78883706..6a1c32e4138 100644
--- a/tests/components/somfy/test_config_flow.py
+++ b/tests/components/somfy/test_config_flow.py
@@ -82,7 +82,9 @@ async def test_full_flow(
},
)
- with patch("homeassistant.components.somfy.api.ConfigEntrySomfyApi"):
+ with patch(
+ "homeassistant.components.somfy.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["data"]["auth_implementation"] == DOMAIN
@@ -95,12 +97,7 @@ async def test_full_flow(
"expires_in": 60,
}
- assert DOMAIN in hass.config.components
- entry = hass.config_entries.async_entries(DOMAIN)[0]
- assert entry.state is config_entries.ConfigEntryState.LOADED
-
- assert await hass.config_entries.async_unload(entry.entry_id)
- assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
+ assert len(mock_setup_entry.mock_calls) == 1
async def test_abort_if_authorization_timeout(hass, current_request_with_host):
diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py
index 62fd3254d60..7c5b4ac91ef 100644
--- a/tests/components/sonos/conftest.py
+++ b/tests/components/sonos/conftest.py
@@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch as patch
import pytest
+from homeassistant.components import ssdp
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.components.sonos import DOMAIN
from homeassistant.const import CONF_HOSTS
@@ -53,6 +54,7 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock):
"socket.gethostbyname", return_value="192.168.42.2"
):
mock_soco = mock.return_value
+ mock_soco.ip_address = "192.168.42.2"
mock_soco.uid = "RINCON_test"
mock_soco.play_mode = "NORMAL"
mock_soco.music_library = music_library
@@ -76,11 +78,18 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock):
def discover_fixture(soco):
"""Create a mock pysonos discover fixture."""
- def do_callback(callback, **kwargs):
- callback(soco)
+ def do_callback(hass, callback, *args, **kwargs):
+ callback(
+ {
+ ssdp.ATTR_UPNP_UDN: soco.uid,
+ ssdp.ATTR_SSDP_LOCATION: f"http://{soco.ip_address}/",
+ }
+ )
return MagicMock()
- with patch("pysonos.discover_thread", side_effect=do_callback) as mock:
+ with patch(
+ "homeassistant.components.ssdp.async_register_callback", side_effect=do_callback
+ ) as mock:
yield mock
diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py
index 41cb241d377..f684a8f351e 100644
--- a/tests/components/sonos/test_switch.py
+++ b/tests/components/sonos/test_switch.py
@@ -1,4 +1,6 @@
"""Tests for the Sonos Alarm switch platform."""
+from copy import copy
+
from homeassistant.components.sonos import DOMAIN
from homeassistant.components.sonos.switch import (
ATTR_DURATION,
@@ -52,24 +54,31 @@ async def test_alarm_create_delete(
hass, config_entry, config, soco, alarm_clock, alarm_clock_extended, alarm_event
):
"""Test for correct creation and deletion of alarms during runtime."""
- soco.alarmClock = alarm_clock_extended
+ entity_registry = async_get_entity_registry(hass)
+
+ one_alarm = copy(alarm_clock.ListAlarms.return_value)
+ two_alarms = copy(alarm_clock_extended.ListAlarms.return_value)
await setup_platform(hass, config_entry, config)
- subscription = alarm_clock_extended.subscribe.return_value
+ assert "switch.sonos_alarm_14" in entity_registry.entities
+ assert "switch.sonos_alarm_15" not in entity_registry.entities
+
+ subscription = alarm_clock.subscribe.return_value
sub_callback = subscription.callback
+ alarm_clock.ListAlarms.return_value = two_alarms
+
sub_callback(event=alarm_event)
await hass.async_block_till_done()
- entity_registry = async_get_entity_registry(hass)
-
assert "switch.sonos_alarm_14" in entity_registry.entities
assert "switch.sonos_alarm_15" in entity_registry.entities
- alarm_clock_extended.ListAlarms.return_value = alarm_clock.ListAlarms.return_value
alarm_event.increment_variable("alarm_list_version")
+ alarm_clock.ListAlarms.return_value = one_alarm
+
sub_callback(event=alarm_event)
await hass.async_block_till_done()
diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py
index 78b0f9e05b6..6c019f1f311 100644
--- a/tests/components/ssdp/test_init.py
+++ b/tests/components/ssdp/test_init.py
@@ -1,42 +1,74 @@
"""Test the SSDP integration."""
import asyncio
from datetime import timedelta
+from ipaddress import IPv4Address, IPv6Address
from unittest.mock import patch
import aiohttp
+from async_upnp_client.search import SSDPListener
+from async_upnp_client.utils import CaseInsensitiveDict
import pytest
from homeassistant import config_entries
from homeassistant.components import ssdp
-from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
+from homeassistant.const import (
+ EVENT_HOMEASSISTANT_STARTED,
+ EVENT_HOMEASSISTANT_STOP,
+ MATCH_ALL,
+)
+from homeassistant.core import CoreState, callback
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed, mock_coro
-async def test_scan_match_st(hass, caplog):
- """Test matching based on ST."""
- scanner = ssdp.Scanner(hass, {"mock-domain": [{"st": "mock-st"}]})
+def _patched_ssdp_listener(info, *args, **kwargs):
+ listener = SSDPListener(*args, **kwargs)
- async def _mock_async_scan(*args, async_callback=None, **kwargs):
- await async_callback(
- {
- "st": "mock-st",
- "location": None,
- "usn": "mock-usn",
- "server": "mock-server",
- "ext": "",
- }
+ async def _async_callback(*_):
+ await listener.async_callback(info)
+
+ listener.async_start = _async_callback
+ return listener
+
+
+async def _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp):
+ def _generate_fake_ssdp_listener(*args, **kwargs):
+ return _patched_ssdp_listener(
+ mock_ssdp_response,
+ *args,
+ **kwargs,
)
with patch(
- "homeassistant.components.ssdp.async_search",
- side_effect=_mock_async_scan,
+ "homeassistant.components.ssdp.async_get_ssdp",
+ return_value=mock_get_ssdp,
+ ), patch(
+ "homeassistant.components.ssdp.SSDPListener",
+ new=_generate_fake_ssdp_listener,
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
- await scanner.async_scan(None)
+ assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ return mock_init
+
+
+async def test_scan_match_st(hass, caplog):
+ """Test matching based on ST."""
+ mock_ssdp_response = {
+ "st": "mock-st",
+ "location": None,
+ "usn": "mock-usn",
+ "server": "mock-server",
+ "ext": "",
+ }
+ mock_get_ssdp = {"mock-domain": [{"st": "mock-st"}]}
+ mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp)
assert len(mock_init.mock_calls) == 1
assert mock_init.mock_calls[0][1][0] == "mock-domain"
@@ -53,6 +85,19 @@ async def test_scan_match_st(hass, caplog):
assert "Failed to fetch ssdp data" not in caplog.text
+async def test_partial_response(hass, caplog):
+ """Test location and st missing."""
+ mock_ssdp_response = {
+ "usn": "mock-usn",
+ "server": "mock-server",
+ "ext": "",
+ }
+ mock_get_ssdp = {"mock-domain": [{"st": "mock-st"}]}
+ mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp)
+
+ assert len(mock_init.mock_calls) == 0
+
+
@pytest.mark.parametrize(
"key", (ssdp.ATTR_UPNP_MANUFACTURER, ssdp.ATTR_UPNP_DEVICE_TYPE)
)
@@ -68,26 +113,13 @@ async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key):
""",
)
- scanner = ssdp.Scanner(hass, {"mock-domain": [{key: "Paulus"}]})
-
- async def _mock_async_scan(*args, async_callback=None, **kwargs):
- for _ in range(5):
- await async_callback(
- {
- "st": "mock-st",
- "location": "http://1.1.1.1",
- }
- )
-
- with patch(
- "homeassistant.components.ssdp.async_search",
- side_effect=_mock_async_scan,
- ), patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
- await scanner.async_scan(None)
-
- # If we get duplicate respones, ensure we only look it up once
+ mock_get_ssdp = {"mock-domain": [{key: "Paulus"}]}
+ mock_ssdp_response = {
+ "st": "mock-st",
+ "location": "http://1.1.1.1",
+ }
+ mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp)
+ # If we get duplicate response, ensure we only look it up once
assert len(aioclient_mock.mock_calls) == 1
assert len(mock_init.mock_calls) == 1
assert mock_init.mock_calls[0][1][0] == "mock-domain"
@@ -108,33 +140,19 @@ async def test_scan_not_all_present(hass, aioclient_mock):
""",
)
- scanner = ssdp.Scanner(
- hass,
- {
- "mock-domain": [
- {
- ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
- ssdp.ATTR_UPNP_MANUFACTURER: "Paulus",
- }
- ]
- },
- )
-
- async def _mock_async_scan(*args, async_callback=None, **kwargs):
- await async_callback(
+ mock_ssdp_response = {
+ "st": "mock-st",
+ "location": "http://1.1.1.1",
+ }
+ mock_get_ssdp = {
+ "mock-domain": [
{
- "st": "mock-st",
- "location": "http://1.1.1.1",
+ ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
+ ssdp.ATTR_UPNP_MANUFACTURER: "Paulus",
}
- )
-
- with patch(
- "homeassistant.components.ssdp.async_search",
- side_effect=_mock_async_scan,
- ), patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
- await scanner.async_scan(None)
+ ]
+ }
+ mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp)
assert not mock_init.mock_calls
@@ -152,33 +170,19 @@ async def test_scan_not_all_match(hass, aioclient_mock):
""",
)
- scanner = ssdp.Scanner(
- hass,
- {
- "mock-domain": [
- {
- ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
- ssdp.ATTR_UPNP_MANUFACTURER: "Not-Paulus",
- }
- ]
- },
- )
-
- async def _mock_async_scan(*args, async_callback=None, **kwargs):
- await async_callback(
+ mock_ssdp_response = {
+ "st": "mock-st",
+ "location": "http://1.1.1.1",
+ }
+ mock_get_ssdp = {
+ "mock-domain": [
{
- "st": "mock-st",
- "location": "http://1.1.1.1",
+ ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
+ ssdp.ATTR_UPNP_MANUFACTURER: "Not-Paulus",
}
- )
-
- with patch(
- "homeassistant.components.ssdp.async_search",
- side_effect=_mock_async_scan,
- ), patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
- await scanner.async_scan(None)
+ ]
+ }
+ mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp)
assert not mock_init.mock_calls
@@ -187,21 +191,31 @@ async def test_scan_not_all_match(hass, aioclient_mock):
async def test_scan_description_fetch_fail(hass, aioclient_mock, exc):
"""Test failing to fetch description."""
aioclient_mock.get("http://1.1.1.1", exc=exc)
- scanner = ssdp.Scanner(hass, {})
-
- async def _mock_async_scan(*args, async_callback=None, **kwargs):
- await async_callback(
+ mock_ssdp_response = {
+ "st": "mock-st",
+ "usn": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3",
+ "location": "http://1.1.1.1",
+ }
+ mock_get_ssdp = {
+ "mock-domain": [
{
- "st": "mock-st",
- "location": "http://1.1.1.1",
+ ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
+ ssdp.ATTR_UPNP_MANUFACTURER: "Paulus",
}
- )
+ ]
+ }
+ mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp)
- with patch(
- "homeassistant.components.ssdp.async_search",
- side_effect=_mock_async_scan,
- ):
- await scanner.async_scan(None)
+ assert not mock_init.mock_calls
+
+ assert ssdp.async_get_discovery_info_by_st(hass, "mock-st") == [
+ {
+ "UDN": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
+ "ssdp_location": "http://1.1.1.1",
+ "ssdp_st": "mock-st",
+ "ssdp_usn": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3",
+ }
+ ]
async def test_scan_description_parse_fail(hass, aioclient_mock):
@@ -212,21 +226,22 @@ async def test_scan_description_parse_fail(hass, aioclient_mock):
INVALIDXML
""",
)
- scanner = ssdp.Scanner(hass, {})
- async def _mock_async_scan(*args, async_callback=None, **kwargs):
- await async_callback(
+ mock_ssdp_response = {
+ "st": "mock-st",
+ "location": "http://1.1.1.1",
+ }
+ mock_get_ssdp = {
+ "mock-domain": [
{
- "st": "mock-st",
- "location": "http://1.1.1.1",
+ ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
+ ssdp.ATTR_UPNP_MANUFACTURER: "Paulus",
}
- )
+ ]
+ }
+ mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp)
- with patch(
- "homeassistant.components.ssdp.async_search",
- side_effect=_mock_async_scan,
- ):
- await scanner.async_scan(None)
+ assert not mock_init.mock_calls
async def test_invalid_characters(hass, aioclient_mock):
@@ -242,32 +257,20 @@ async def test_invalid_characters(hass, aioclient_mock):
""",
)
- scanner = ssdp.Scanner(
- hass,
- {
- "mock-domain": [
- {
- ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC",
- }
- ]
- },
- )
- async def _mock_async_scan(*args, async_callback=None, **kwargs):
- await async_callback(
+ mock_ssdp_response = {
+ "st": "mock-st",
+ "location": "http://1.1.1.1",
+ }
+ mock_get_ssdp = {
+ "mock-domain": [
{
- "st": "mock-st",
- "location": "http://1.1.1.1",
+ ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC",
}
- )
+ ]
+ }
- with patch(
- "homeassistant.components.ssdp.async_search",
- side_effect=_mock_async_scan,
- ), patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
- await scanner.async_scan(None)
+ mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp)
assert len(mock_init.mock_calls) == 1
assert mock_init.mock_calls[0][1][0] == "mock-domain"
@@ -282,8 +285,9 @@ async def test_invalid_characters(hass, aioclient_mock):
}
-@patch("homeassistant.components.ssdp.async_search")
-async def test_start_stop_scanner(async_search_mock, hass):
+@patch("homeassistant.components.ssdp.SSDPListener.async_start")
+@patch("homeassistant.components.ssdp.SSDPListener.async_search")
+async def test_start_stop_scanner(async_start_mock, async_search_mock, hass):
"""Test we start and stop the scanner."""
assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
@@ -291,13 +295,15 @@ async def test_start_stop_scanner(async_search_mock, hass):
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
await hass.async_block_till_done()
- assert async_search_mock.call_count == 2
+ assert async_start_mock.call_count == 1
+ assert async_search_mock.call_count == 1
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
await hass.async_block_till_done()
- assert async_search_mock.call_count == 2
+ assert async_start_mock.call_count == 1
+ assert async_search_mock.call_count == 1
async def test_unexpected_exception_while_fetching(hass, aioclient_mock, caplog):
@@ -313,34 +319,469 @@ async def test_unexpected_exception_while_fetching(hass, aioclient_mock, caplog)
""",
)
- scanner = ssdp.Scanner(
- hass,
- {
- "mock-domain": [
- {
- ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC",
- }
- ]
- },
- )
-
- async def _mock_async_scan(*args, async_callback=None, **kwargs):
- await async_callback(
+ mock_ssdp_response = {
+ "st": "mock-st",
+ "location": "http://1.1.1.1",
+ }
+ mock_get_ssdp = {
+ "mock-domain": [
{
- "st": "mock-st",
- "location": "http://1.1.1.1",
+ ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC",
}
- )
+ ]
+ }
with patch(
- "homeassistant.components.ssdp.ElementTree.fromstring", side_effect=ValueError
- ), patch(
- "homeassistant.components.ssdp.async_search",
- side_effect=_mock_async_scan,
- ), patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
- await scanner.async_scan(None)
+ "homeassistant.components.ssdp.descriptions.ElementTree.fromstring",
+ side_effect=ValueError,
+ ):
+ mock_init = await _async_run_mocked_scan(
+ hass, mock_ssdp_response, mock_get_ssdp
+ )
assert len(mock_init.mock_calls) == 0
assert "Failed to fetch ssdp data from: http://1.1.1.1" in caplog.text
+
+
+async def test_scan_with_registered_callback(hass, aioclient_mock, caplog):
+ """Test matching based on callback."""
+ aioclient_mock.get(
+ "http://1.1.1.1",
+ text="""
+
+
+ Paulus
+
+
+ """,
+ )
+ mock_ssdp_response = {
+ "st": "mock-st",
+ "location": "http://1.1.1.1",
+ "usn": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3",
+ "server": "mock-server",
+ "x-rincon-bootseq": "55",
+ "ext": "",
+ }
+ not_matching_integration_callbacks = []
+ integration_match_all_callbacks = []
+ integration_match_all_not_present_callbacks = []
+ integration_callbacks = []
+ integration_callbacks_from_cache = []
+ match_any_callbacks = []
+
+ @callback
+ def _async_exception_callbacks(info):
+ raise ValueError
+
+ @callback
+ def _async_integration_callbacks(info):
+ integration_callbacks.append(info)
+
+ @callback
+ def _async_integration_match_all_callbacks(info):
+ integration_match_all_callbacks.append(info)
+
+ @callback
+ def _async_integration_match_all_not_present_callbacks(info):
+ integration_match_all_not_present_callbacks.append(info)
+
+ @callback
+ def _async_integration_callbacks_from_cache(info):
+ integration_callbacks_from_cache.append(info)
+
+ @callback
+ def _async_not_matching_integration_callbacks(info):
+ not_matching_integration_callbacks.append(info)
+
+ @callback
+ def _async_match_any_callbacks(info):
+ match_any_callbacks.append(info)
+
+ def _generate_fake_ssdp_listener(*args, **kwargs):
+ listener = SSDPListener(*args, **kwargs)
+
+ async def _async_callback(*_):
+ await listener.async_callback(mock_ssdp_response)
+
+ @callback
+ def _callback(*_):
+ hass.async_create_task(listener.async_callback(mock_ssdp_response))
+
+ listener.async_start = _async_callback
+ listener.async_search = _callback
+ return listener
+
+ with patch(
+ "homeassistant.components.ssdp.SSDPListener",
+ new=_generate_fake_ssdp_listener,
+ ):
+ hass.state = CoreState.stopped
+ assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
+ await hass.async_block_till_done()
+ ssdp.async_register_callback(hass, _async_exception_callbacks, {})
+ ssdp.async_register_callback(
+ hass,
+ _async_integration_callbacks,
+ {"st": "mock-st"},
+ )
+ ssdp.async_register_callback(
+ hass,
+ _async_integration_match_all_callbacks,
+ {"x-rincon-bootseq": MATCH_ALL},
+ )
+ ssdp.async_register_callback(
+ hass,
+ _async_integration_match_all_not_present_callbacks,
+ {"x-not-there": MATCH_ALL},
+ )
+ ssdp.async_register_callback(
+ hass,
+ _async_not_matching_integration_callbacks,
+ {"st": "not-match-mock-st"},
+ )
+ ssdp.async_register_callback(
+ hass,
+ _async_match_any_callbacks,
+ )
+ await hass.async_block_till_done()
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
+ ssdp.async_register_callback(
+ hass,
+ _async_integration_callbacks_from_cache,
+ {"st": "mock-st"},
+ )
+ await hass.async_block_till_done()
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.state = CoreState.running
+ await hass.async_block_till_done()
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
+ await hass.async_block_till_done()
+ assert hass.state == CoreState.running
+
+ assert len(integration_callbacks) == 3
+ assert len(integration_callbacks_from_cache) == 3
+ assert len(integration_match_all_callbacks) == 3
+ assert len(integration_match_all_not_present_callbacks) == 0
+ assert len(match_any_callbacks) == 3
+ assert len(not_matching_integration_callbacks) == 0
+ assert integration_callbacks[0] == {
+ ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
+ ssdp.ATTR_SSDP_EXT: "",
+ ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1",
+ ssdp.ATTR_SSDP_SERVER: "mock-server",
+ ssdp.ATTR_SSDP_ST: "mock-st",
+ ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3",
+ ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
+ "x-rincon-bootseq": "55",
+ }
+ assert "Failed to callback info" in caplog.text
+
+
+async def test_unsolicited_ssdp_registered_callback(hass, aioclient_mock, caplog):
+ """Test matching based on callback can handle unsolicited ssdp traffic without st."""
+ aioclient_mock.get(
+ "http://10.6.9.12:1400/xml/device_description.xml",
+ text="""
+
+
+ Paulus
+
+
+ """,
+ )
+ mock_ssdp_response = {
+ "location": "http://10.6.9.12:1400/xml/device_description.xml",
+ "nt": "uuid:RINCON_1111BB963FD801400",
+ "nts": "ssdp:alive",
+ "server": "Linux UPnP/1.0 Sonos/63.2-88230 (ZPS12)",
+ "usn": "uuid:RINCON_1111BB963FD801400",
+ "x-rincon-household": "Sonos_dfjfkdghjhkjfhkdjfhkd",
+ "x-rincon-bootseq": "250",
+ "bootid.upnp.org": "250",
+ "x-rincon-wifimode": "0",
+ "x-rincon-variant": "1",
+ "household.smartspeaker.audio": "Sonos_v3294823948542543534",
+ }
+ integration_callbacks = []
+
+ @callback
+ def _async_integration_callbacks(info):
+ integration_callbacks.append(info)
+
+ def _generate_fake_ssdp_listener(*args, **kwargs):
+ listener = SSDPListener(*args, **kwargs)
+
+ async def _async_callback(*_):
+ await listener.async_callback(mock_ssdp_response)
+
+ @callback
+ def _callback(*_):
+ hass.async_create_task(listener.async_callback(mock_ssdp_response))
+
+ listener.async_start = _async_callback
+ listener.async_search = _callback
+ return listener
+
+ with patch(
+ "homeassistant.components.ssdp.SSDPListener",
+ new=_generate_fake_ssdp_listener,
+ ):
+ hass.state = CoreState.stopped
+ assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
+ await hass.async_block_till_done()
+ ssdp.async_register_callback(
+ hass,
+ _async_integration_callbacks,
+ {"nts": "ssdp:alive", "x-rincon-bootseq": MATCH_ALL},
+ )
+ await hass.async_block_till_done()
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
+ await hass.async_block_till_done()
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.state = CoreState.running
+ await hass.async_block_till_done()
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
+ await hass.async_block_till_done()
+ assert hass.state == CoreState.running
+
+ assert (
+ len(integration_callbacks) == 2
+ ) # unsolicited callbacks without st are not cached
+ assert integration_callbacks[0] == {
+ "UDN": "uuid:RINCON_1111BB963FD801400",
+ "bootid.upnp.org": "250",
+ "deviceType": "Paulus",
+ "household.smartspeaker.audio": "Sonos_v3294823948542543534",
+ "nt": "uuid:RINCON_1111BB963FD801400",
+ "nts": "ssdp:alive",
+ "ssdp_location": "http://10.6.9.12:1400/xml/device_description.xml",
+ "ssdp_server": "Linux UPnP/1.0 Sonos/63.2-88230 (ZPS12)",
+ "ssdp_usn": "uuid:RINCON_1111BB963FD801400",
+ "x-rincon-bootseq": "250",
+ "x-rincon-household": "Sonos_dfjfkdghjhkjfhkdjfhkd",
+ "x-rincon-variant": "1",
+ "x-rincon-wifimode": "0",
+ }
+ assert "Failed to callback info" not in caplog.text
+
+
+async def test_scan_second_hit(hass, aioclient_mock, caplog):
+ """Test matching on second scan."""
+ aioclient_mock.get(
+ "http://1.1.1.1",
+ text="""
+
+
+ Paulus
+
+
+ """,
+ )
+
+ mock_ssdp_response = CaseInsensitiveDict(
+ **{
+ "ST": "mock-st",
+ "LOCATION": "http://1.1.1.1",
+ "USN": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3",
+ "SERVER": "mock-server",
+ "EXT": "",
+ }
+ )
+ mock_get_ssdp = {"mock-domain": [{"st": "mock-st"}]}
+ integration_callbacks = []
+
+ @callback
+ def _async_integration_callbacks(info):
+ integration_callbacks.append(info)
+
+ def _generate_fake_ssdp_listener(*args, **kwargs):
+ listener = SSDPListener(*args, **kwargs)
+
+ async def _async_callback(*_):
+ pass
+
+ @callback
+ def _callback(*_):
+ hass.async_create_task(listener.async_callback(mock_ssdp_response))
+
+ listener.async_start = _async_callback
+ listener.async_search = _callback
+ return listener
+
+ with patch(
+ "homeassistant.components.ssdp.async_get_ssdp",
+ return_value=mock_get_ssdp,
+ ), patch(
+ "homeassistant.components.ssdp.SSDPListener",
+ new=_generate_fake_ssdp_listener,
+ ), patch.object(
+ hass.config_entries.flow, "async_init", return_value=mock_coro()
+ ) as mock_init:
+ assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
+ await hass.async_block_till_done()
+ remove = ssdp.async_register_callback(
+ hass,
+ _async_integration_callbacks,
+ {"st": "mock-st"},
+ )
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ await hass.async_block_till_done()
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
+ await hass.async_block_till_done()
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
+ await hass.async_block_till_done()
+ remove()
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
+ await hass.async_block_till_done()
+
+ assert len(integration_callbacks) == 2
+ assert integration_callbacks[0] == {
+ ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
+ ssdp.ATTR_SSDP_EXT: "",
+ ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1",
+ ssdp.ATTR_SSDP_SERVER: "mock-server",
+ ssdp.ATTR_SSDP_ST: "mock-st",
+ ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3",
+ ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
+ }
+ assert len(mock_init.mock_calls) == 1
+ assert mock_init.mock_calls[0][1][0] == "mock-domain"
+ assert mock_init.mock_calls[0][2]["context"] == {
+ "source": config_entries.SOURCE_SSDP
+ }
+ assert mock_init.mock_calls[0][2]["data"] == {
+ ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
+ ssdp.ATTR_SSDP_ST: "mock-st",
+ ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1",
+ ssdp.ATTR_SSDP_SERVER: "mock-server",
+ ssdp.ATTR_SSDP_EXT: "",
+ ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3",
+ ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
+ }
+ assert "Failed to fetch ssdp data" not in caplog.text
+ udn_discovery_info = ssdp.async_get_discovery_info_by_st(hass, "mock-st")
+ discovery_info = udn_discovery_info[0]
+ assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1"
+ assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st"
+ assert (
+ discovery_info[ssdp.ATTR_UPNP_UDN]
+ == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
+ )
+ assert (
+ discovery_info[ssdp.ATTR_SSDP_USN]
+ == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3"
+ )
+
+ st_discovery_info = ssdp.async_get_discovery_info_by_udn(
+ hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
+ )
+ discovery_info = st_discovery_info[0]
+ assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1"
+ assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st"
+ assert (
+ discovery_info[ssdp.ATTR_UPNP_UDN]
+ == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
+ )
+ assert (
+ discovery_info[ssdp.ATTR_SSDP_USN]
+ == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3"
+ )
+
+ discovery_info = ssdp.async_get_discovery_info_by_udn_st(
+ hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", "mock-st"
+ )
+ assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1"
+ assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st"
+ assert (
+ discovery_info[ssdp.ATTR_UPNP_UDN]
+ == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
+ )
+ assert (
+ discovery_info[ssdp.ATTR_SSDP_USN]
+ == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3"
+ )
+
+ assert ssdp.async_get_discovery_info_by_udn_st(hass, "wrong", "mock-st") is None
+
+
+_ADAPTERS_WITH_MANUAL_CONFIG = [
+ {
+ "auto": True,
+ "default": False,
+ "enabled": True,
+ "ipv4": [],
+ "ipv6": [
+ {
+ "address": "2001:db8::",
+ "network_prefix": 8,
+ "flowinfo": 1,
+ "scope_id": 1,
+ }
+ ],
+ "name": "eth0",
+ },
+ {
+ "auto": True,
+ "default": False,
+ "enabled": True,
+ "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
+ "ipv6": [],
+ "name": "eth1",
+ },
+ {
+ "auto": False,
+ "default": False,
+ "enabled": False,
+ "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}],
+ "ipv6": [],
+ "name": "vtun0",
+ },
+]
+
+
+async def test_async_detect_interfaces_setting_empty_route(hass):
+ """Test without default interface config and the route returns nothing."""
+ mock_get_ssdp = {
+ "mock-domain": [
+ {
+ ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC",
+ }
+ ]
+ }
+ create_args = []
+
+ def _generate_fake_ssdp_listener(*args, **kwargs):
+ create_args.append([args, kwargs])
+ listener = SSDPListener(*args, **kwargs)
+
+ async def _async_callback(*_):
+ pass
+
+ @callback
+ def _callback(*_):
+ pass
+
+ listener.async_start = _async_callback
+ listener.async_search = _callback
+ return listener
+
+ with patch(
+ "homeassistant.components.ssdp.async_get_ssdp",
+ return_value=mock_get_ssdp,
+ ), patch(
+ "homeassistant.components.ssdp.SSDPListener",
+ new=_generate_fake_ssdp_listener,
+ ), patch(
+ "homeassistant.components.ssdp.network.async_get_adapters",
+ return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
+ ):
+ assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
+ await hass.async_block_till_done()
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ await hass.async_block_till_done()
+
+ assert {create_args[0][1]["source_ip"], create_args[1][1]["source_ip"]} == {
+ IPv4Address("192.168.1.5"),
+ IPv6Address("2001:db8::"),
+ }
diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py
index 60de732cf79..bcbf13b8298 100644
--- a/tests/components/statistics/test_sensor.py
+++ b/tests/components/statistics/test_sensor.py
@@ -48,6 +48,9 @@ class TestStatisticsSensor(unittest.TestCase):
self.median = round(statistics.median(self.values), 2)
self.deviation = round(statistics.stdev(self.values), 2)
self.variance = round(statistics.variance(self.values), 2)
+ self.quantiles = [
+ round(quantile, 2) for quantile in statistics.quantiles(self.values)
+ ]
self.change = round(self.values[-1] - self.values[0], 2)
self.average_change = round(self.change / (len(self.values) - 1), 2)
self.change_rate = round(self.change / (60 * (self.count - 1)), 2)
@@ -112,6 +115,7 @@ class TestStatisticsSensor(unittest.TestCase):
assert self.variance == state.attributes.get("variance")
assert self.median == state.attributes.get("median")
assert self.deviation == state.attributes.get("standard_deviation")
+ assert self.quantiles == state.attributes.get("quantiles")
assert self.mean == state.attributes.get("mean")
assert self.count == state.attributes.get("count")
assert self.total == state.attributes.get("total")
@@ -188,6 +192,7 @@ class TestStatisticsSensor(unittest.TestCase):
# require at least two data points
assert state.attributes.get("variance") == STATE_UNKNOWN
assert state.attributes.get("standard_deviation") == STATE_UNKNOWN
+ assert state.attributes.get("quantiles") == STATE_UNKNOWN
def test_max_age(self):
"""Test value deprecation."""
diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py
index 4c6841d03db..a39e8bdca21 100644
--- a/tests/components/stream/common.py
+++ b/tests/components/stream/common.py
@@ -8,26 +8,27 @@ import numpy as np
AUDIO_SAMPLE_RATE = 8000
-def generate_h264_video(container_format="mp4", audio_codec=None):
+def generate_audio_frame(pcm_mulaw=False):
+ """Generate a blank audio frame."""
+ if pcm_mulaw:
+ audio_frame = av.AudioFrame(format="s16", layout="mono", samples=1)
+ audio_bytes = b"\x00\x00"
+ else:
+ audio_frame = av.AudioFrame(format="dbl", layout="mono", samples=1024)
+ audio_bytes = b"\x00\x00\x00\x00\x00\x00\x00\x00" * 1024
+ audio_frame.planes[0].update(audio_bytes)
+ audio_frame.sample_rate = AUDIO_SAMPLE_RATE
+ audio_frame.time_base = Fraction(1, AUDIO_SAMPLE_RATE)
+ return audio_frame
+
+
+def generate_h264_video(container_format="mp4"):
"""
Generate a test video.
See: http://docs.mikeboers.com/pyav/develop/cookbook/numpy.html
"""
- def generate_audio_frame(pcm_mulaw=False):
- """Generate a blank audio frame."""
- if pcm_mulaw:
- audio_frame = av.AudioFrame(format="s16", layout="mono", samples=1)
- audio_bytes = b"\x00\x00"
- else:
- audio_frame = av.AudioFrame(format="dbl", layout="mono", samples=1024)
- audio_bytes = b"\x00\x00\x00\x00\x00\x00\x00\x00" * 1024
- audio_frame.planes[0].update(audio_bytes)
- audio_frame.sample_rate = AUDIO_SAMPLE_RATE
- audio_frame.time_base = Fraction(1, AUDIO_SAMPLE_RATE)
- return audio_frame
-
duration = 5
fps = 24
total_frames = duration * fps
@@ -42,6 +43,39 @@ def generate_h264_video(container_format="mp4", audio_codec=None):
stream.pix_fmt = "yuv420p"
stream.options.update({"g": str(fps), "keyint_min": str(fps)})
+ for frame_i in range(total_frames):
+
+ img = np.empty((480, 320, 3))
+ img[:, :, 0] = 0.5 + 0.5 * np.sin(2 * np.pi * (0 / 3 + frame_i / total_frames))
+ img[:, :, 1] = 0.5 + 0.5 * np.sin(2 * np.pi * (1 / 3 + frame_i / total_frames))
+ img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames))
+
+ img = np.round(255 * img).astype(np.uint8)
+ img = np.clip(img, 0, 255)
+
+ frame = av.VideoFrame.from_ndarray(img, format="rgb24")
+ for packet in stream.encode(frame):
+ container.mux(packet)
+
+ # Flush stream
+ for packet in stream.encode():
+ container.mux(packet)
+
+ # Close the file
+ container.close()
+ output.seek(0)
+
+ return output
+
+
+def remux_with_audio(source, container_format, audio_codec):
+ """Remux an existing source with new audio."""
+ av_source = av.open(source, mode="r")
+ output = io.BytesIO()
+ output.name = "test.mov" if container_format == "mov" else "test.mp4"
+ container = av.open(output, mode="w", format=container_format)
+ container.add_stream(template=av_source.streams.video[0])
+
a_packet = None
last_a_dts = -1
if audio_codec is not None:
@@ -57,23 +91,17 @@ def generate_h264_video(container_format="mp4", audio_codec=None):
if a_packets:
a_packet = a_packets[0]
- for frame_i in range(total_frames):
-
- img = np.empty((480, 320, 3))
- img[:, :, 0] = 0.5 + 0.5 * np.sin(2 * np.pi * (0 / 3 + frame_i / total_frames))
- img[:, :, 1] = 0.5 + 0.5 * np.sin(2 * np.pi * (1 / 3 + frame_i / total_frames))
- img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames))
-
- img = np.round(255 * img).astype(np.uint8)
- img = np.clip(img, 0, 255)
-
- frame = av.VideoFrame.from_ndarray(img, format="rgb24")
- for packet in stream.encode(frame):
- container.mux(packet)
-
+ # open original source and iterate through video packets
+ for packet in av_source.demux(video=0):
+ if not packet.dts:
+ continue
+ container.mux(packet)
if a_packet is not None:
- a_packet.pts = int(frame_i / (fps * a_packet.time_base))
- while a_packet.pts * a_packet.time_base * fps < frame_i + 1:
+ a_packet.pts = int(packet.dts * packet.time_base / a_packet.time_base)
+ while (
+ a_packet.pts * a_packet.time_base
+ < (packet.dts + packet.duration) * packet.time_base
+ ):
a_packet.dts = a_packet.pts
if (
a_packet.dts > last_a_dts
@@ -82,10 +110,6 @@ def generate_h264_video(container_format="mp4", audio_codec=None):
last_a_dts = a_packet.dts
a_packet.pts += a_packet.duration
- # Flush stream
- for packet in stream.encode():
- container.mux(packet)
-
# Close the file
container.close()
output.seek(0)
diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py
index ead2018b528..a73678d763f 100644
--- a/tests/components/stream/conftest.py
+++ b/tests/components/stream/conftest.py
@@ -9,13 +9,21 @@ nothing for the test to verify. The solution is the WorkerSync class that
allows the tests to pause the worker thread before finalizing the stream
so that it can inspect the output.
"""
+from __future__ import annotations
+
+import asyncio
+from collections import deque
import logging
import threading
from unittest.mock import patch
+import async_timeout
import pytest
from homeassistant.components.stream import Stream
+from homeassistant.components.stream.core import Segment
+
+TEST_TIMEOUT = 7.0 # Lower than 9s home assistant timeout
class WorkerSync:
@@ -58,3 +66,57 @@ def stream_worker_sync(hass):
autospec=True,
):
yield sync
+
+
+class SaveRecordWorkerSync:
+ """
+ Test fixture to manage RecordOutput thread for recorder_save_worker.
+
+ This is used to assert that the worker is started and stopped cleanly
+ to avoid thread leaks in tests.
+ """
+
+ def __init__(self):
+ """Initialize SaveRecordWorkerSync."""
+ self._save_event = None
+ self._segments = None
+ self._save_thread = None
+ self.reset()
+
+ def recorder_save_worker(self, file_out: str, segments: deque[Segment]):
+ """Mock method for patch."""
+ logging.debug("recorder_save_worker thread started")
+ assert self._save_thread is None
+ self._segments = segments
+ self._save_thread = threading.current_thread()
+ self._save_event.set()
+
+ async def get_segments(self):
+ """Return the recorded video segments."""
+ with async_timeout.timeout(TEST_TIMEOUT):
+ await self._save_event.wait()
+ return self._segments
+
+ async def join(self):
+ """Verify save worker was invoked and block on shutdown."""
+ with async_timeout.timeout(TEST_TIMEOUT):
+ await self._save_event.wait()
+ self._save_thread.join(timeout=TEST_TIMEOUT)
+ assert not self._save_thread.is_alive()
+
+ def reset(self):
+ """Reset callback state for reuse in tests."""
+ self._save_thread = None
+ self._save_event = asyncio.Event()
+
+
+@pytest.fixture()
+def record_worker_sync(hass):
+ """Patch recorder_save_worker for clean thread shutdown for test."""
+ sync = SaveRecordWorkerSync()
+ with patch(
+ "homeassistant.components.stream.recorder.recorder_save_worker",
+ side_effect=sync.recorder_save_worker,
+ autospec=True,
+ ):
+ yield sync
diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py
index f9b96a662d9..919f71c8509 100644
--- a/tests/components/stream/test_hls.py
+++ b/tests/components/stream/test_hls.py
@@ -1,5 +1,5 @@
"""The tests for hls streams."""
-from datetime import timedelta
+from datetime import datetime, timedelta
from unittest.mock import patch
from urllib.parse import urlparse
@@ -7,8 +7,12 @@ import av
import pytest
from homeassistant.components.stream import create_stream
-from homeassistant.components.stream.const import MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS
-from homeassistant.components.stream.core import Segment
+from homeassistant.components.stream.const import (
+ HLS_PROVIDER,
+ MAX_SEGMENTS,
+ NUM_PLAYLIST_SEGMENTS,
+)
+from homeassistant.components.stream.core import Part, Segment
from homeassistant.const import HTTP_NOT_FOUND
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -18,10 +22,11 @@ from tests.components.stream.common import generate_h264_video
STREAM_SOURCE = "some-stream-source"
INIT_BYTES = b"init"
-MOOF_BYTES = b"some-bytes"
-DURATION = 10
+FAKE_PAYLOAD = b"fake-payload"
+SEGMENT_DURATION = 10
TEST_TIMEOUT = 5.0 # Lower than 9s home assistant timeout
MAX_ABORT_SEGMENTS = 20 # Abort test to avoid looping forever
+FAKE_TIME = datetime.utcnow()
class HlsClient:
@@ -47,7 +52,7 @@ def hls_stream(hass, hass_client):
async def create_client_for_stream(stream):
http_client = await hass_client()
- parsed_url = urlparse(stream.endpoint_url("hls"))
+ parsed_url = urlparse(stream.endpoint_url(HLS_PROVIDER))
return HlsClient(http_client, parsed_url)
return create_client_for_stream
@@ -57,20 +62,32 @@ def make_segment(segment, discontinuity=False):
"""Create a playlist response for a segment."""
response = []
if discontinuity:
- response.append("#EXT-X-DISCONTINUITY")
- response.extend(["#EXTINF:10.0000,", f"./segment/{segment}.m4s"]),
+ response.extend(
+ [
+ "#EXT-X-DISCONTINUITY",
+ "#EXT-X-PROGRAM-DATE-TIME:"
+ + FAKE_TIME.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
+ + "Z",
+ ]
+ )
+ response.extend([f"#EXTINF:{SEGMENT_DURATION:.3f},", f"./segment/{segment}.m4s"])
return "\n".join(response)
-def make_playlist(sequence, discontinuity_sequence=0, segments=[]):
+def make_playlist(sequence, segments, discontinuity_sequence=0):
"""Create a an hls playlist response for tests to assert on."""
response = [
"#EXTM3U",
- "#EXT-X-VERSION:7",
- "#EXT-X-TARGETDURATION:10",
+ "#EXT-X-VERSION:6",
+ "#EXT-X-INDEPENDENT-SEGMENTS",
'#EXT-X-MAP:URI="init.mp4"',
+ "#EXT-X-TARGETDURATION:10",
f"#EXT-X-MEDIA-SEQUENCE:{sequence}",
f"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuity_sequence}",
+ "#EXT-X-PROGRAM-DATE-TIME:"
+ + FAKE_TIME.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
+ + "Z",
+ f"#EXT-X-START:TIME-OFFSET=-{1.5*SEGMENT_DURATION:.3f}",
]
response.extend(segments)
response.append("")
@@ -90,10 +107,10 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync):
# Setup demo HLS track
source = generate_h264_video()
- stream = create_stream(hass, source)
+ stream = create_stream(hass, source, {})
# Request stream
- stream.add_provider("hls")
+ stream.add_provider(HLS_PROVIDER)
stream.start()
hls_client = await hls_stream(stream)
@@ -131,12 +148,12 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync):
# Setup demo HLS track
source = generate_h264_video()
- stream = create_stream(hass, source)
+ stream = create_stream(hass, source, {})
# Request stream
- stream.add_provider("hls")
+ stream.add_provider(HLS_PROVIDER)
stream.start()
- url = stream.endpoint_url("hls")
+ url = stream.endpoint_url(HLS_PROVIDER)
http_client = await hass_client()
@@ -173,10 +190,10 @@ async def test_stream_timeout_after_stop(hass, hass_client, stream_worker_sync):
# Setup demo HLS track
source = generate_h264_video()
- stream = create_stream(hass, source)
+ stream = create_stream(hass, source, {})
# Request stream
- stream.add_provider("hls")
+ stream.add_provider(HLS_PROVIDER)
stream.start()
stream_worker_sync.resume()
@@ -195,8 +212,8 @@ async def test_stream_keepalive(hass):
# Setup demo HLS track
source = "test_stream_keepalive_source"
- stream = create_stream(hass, source)
- track = stream.add_provider("hls")
+ stream = create_stream(hass, source, {})
+ track = stream.add_provider(HLS_PROVIDER)
track.num_segments = 2
cur_time = 0
@@ -230,8 +247,8 @@ async def test_hls_playlist_view_no_output(hass, hass_client, hls_stream):
"""Test rendering the hls playlist with no output segments."""
await async_setup_component(hass, "stream", {"stream": {}})
- stream = create_stream(hass, STREAM_SOURCE)
- stream.add_provider("hls")
+ stream = create_stream(hass, STREAM_SOURCE, {})
+ stream.add_provider(HLS_PROVIDER)
hls_client = await hls_stream(stream)
@@ -244,25 +261,29 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync):
"""Test rendering the hls playlist with 1 and 2 output segments."""
await async_setup_component(hass, "stream", {"stream": {}})
- stream = create_stream(hass, STREAM_SOURCE)
+ stream = create_stream(hass, STREAM_SOURCE, {})
stream_worker_sync.pause()
- hls = stream.add_provider("hls")
-
- hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION))
+ hls = stream.add_provider(HLS_PROVIDER)
+ for i in range(2):
+ segment = Segment(sequence=i, duration=SEGMENT_DURATION, start_time=FAKE_TIME)
+ hls.put(segment)
await hass.async_block_till_done()
hls_client = await hls_stream(stream)
resp = await hls_client.get("/playlist.m3u8")
assert resp.status == 200
- assert await resp.text() == make_playlist(sequence=1, segments=[make_segment(1)])
+ assert await resp.text() == make_playlist(
+ sequence=0, segments=[make_segment(0), make_segment(1)]
+ )
- hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, DURATION))
+ segment = Segment(sequence=2, duration=SEGMENT_DURATION, start_time=FAKE_TIME)
+ hls.put(segment)
await hass.async_block_till_done()
resp = await hls_client.get("/playlist.m3u8")
assert resp.status == 200
assert await resp.text() == make_playlist(
- sequence=1, segments=[make_segment(1), make_segment(2)]
+ sequence=0, segments=[make_segment(0), make_segment(1), make_segment(2)]
)
stream_worker_sync.resume()
@@ -273,36 +294,47 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync):
"""Test rendering the hls playlist with more segments than the segment deque can hold."""
await async_setup_component(hass, "stream", {"stream": {}})
- stream = create_stream(hass, STREAM_SOURCE)
+ stream = create_stream(hass, STREAM_SOURCE, {})
stream_worker_sync.pause()
- hls = stream.add_provider("hls")
+ hls = stream.add_provider(HLS_PROVIDER)
hls_client = await hls_stream(stream)
# Produce enough segments to overfill the output buffer by one
- for sequence in range(1, MAX_SEGMENTS + 2):
- hls.put(Segment(sequence, INIT_BYTES, MOOF_BYTES, DURATION))
+ for sequence in range(MAX_SEGMENTS + 1):
+ segment = Segment(
+ sequence=sequence, duration=SEGMENT_DURATION, start_time=FAKE_TIME
+ )
+ hls.put(segment)
await hass.async_block_till_done()
resp = await hls_client.get("/playlist.m3u8")
assert resp.status == 200
# Only NUM_PLAYLIST_SEGMENTS are returned in the playlist.
- start = MAX_SEGMENTS + 2 - NUM_PLAYLIST_SEGMENTS
+ start = MAX_SEGMENTS + 1 - NUM_PLAYLIST_SEGMENTS
segments = []
- for sequence in range(start, MAX_SEGMENTS + 2):
+ for sequence in range(start, MAX_SEGMENTS + 1):
segments.append(make_segment(sequence))
- assert await resp.text() == make_playlist(
- sequence=start,
- segments=segments,
- )
+ assert await resp.text() == make_playlist(sequence=start, segments=segments)
+
+ # Fetch the actual segments with a fake byte payload
+ for segment in hls.get_segments():
+ segment.init = INIT_BYTES
+ segment.parts = [
+ Part(
+ duration=SEGMENT_DURATION,
+ has_keyframe=True,
+ data=FAKE_PAYLOAD,
+ )
+ ]
# The segment that fell off the buffer is not accessible
- segment_response = await hls_client.get("/segment/1.m4s")
+ segment_response = await hls_client.get("/segment/0.m4s")
assert segment_response.status == 404
# However all segments in the buffer are accessible, even those that were not in the playlist.
- for sequence in range(2, MAX_SEGMENTS + 2):
+ for sequence in range(1, MAX_SEGMENTS + 1):
segment_response = await hls_client.get(f"/segment/{sequence}.m4s")
assert segment_response.status == 200
@@ -314,13 +346,25 @@ async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_s
"""Test a discontinuity across segments in the stream with 3 segments."""
await async_setup_component(hass, "stream", {"stream": {}})
- stream = create_stream(hass, STREAM_SOURCE)
+ stream = create_stream(hass, STREAM_SOURCE, {})
stream_worker_sync.pause()
- hls = stream.add_provider("hls")
+ hls = stream.add_provider(HLS_PROVIDER)
- hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0))
- hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0))
- hls.put(Segment(3, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=1))
+ segment = Segment(
+ sequence=0, stream_id=0, duration=SEGMENT_DURATION, start_time=FAKE_TIME
+ )
+ hls.put(segment)
+ segment = Segment(
+ sequence=1, stream_id=0, duration=SEGMENT_DURATION, start_time=FAKE_TIME
+ )
+ hls.put(segment)
+ segment = Segment(
+ sequence=2,
+ stream_id=1,
+ duration=SEGMENT_DURATION,
+ start_time=FAKE_TIME,
+ )
+ hls.put(segment)
await hass.async_block_till_done()
hls_client = await hls_stream(stream)
@@ -328,11 +372,11 @@ async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_s
resp = await hls_client.get("/playlist.m3u8")
assert resp.status == 200
assert await resp.text() == make_playlist(
- sequence=1,
+ sequence=0,
segments=[
+ make_segment(0),
make_segment(1),
- make_segment(2),
- make_segment(3, discontinuity=True),
+ make_segment(2, discontinuity=True),
],
)
@@ -344,17 +388,26 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy
"""Test a discontinuity with more segments than the segment deque can hold."""
await async_setup_component(hass, "stream", {"stream": {}})
- stream = create_stream(hass, STREAM_SOURCE)
+ stream = create_stream(hass, STREAM_SOURCE, {})
stream_worker_sync.pause()
- hls = stream.add_provider("hls")
+ hls = stream.add_provider(HLS_PROVIDER)
hls_client = await hls_stream(stream)
- hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0))
+ segment = Segment(
+ sequence=0, stream_id=0, duration=SEGMENT_DURATION, start_time=FAKE_TIME
+ )
+ hls.put(segment)
# Produce enough segments to overfill the output buffer by one
- for sequence in range(1, MAX_SEGMENTS + 2):
- hls.put(Segment(sequence, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=1))
+ for sequence in range(MAX_SEGMENTS + 1):
+ segment = Segment(
+ sequence=sequence,
+ stream_id=1,
+ duration=SEGMENT_DURATION,
+ start_time=FAKE_TIME,
+ )
+ hls.put(segment)
await hass.async_block_till_done()
resp = await hls_client.get("/playlist.m3u8")
@@ -363,9 +416,9 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy
# Only NUM_PLAYLIST_SEGMENTS are returned in the playlist causing the
# EXT-X-DISCONTINUITY tag to be omitted and EXT-X-DISCONTINUITY-SEQUENCE
# returned instead.
- start = MAX_SEGMENTS + 2 - NUM_PLAYLIST_SEGMENTS
+ start = MAX_SEGMENTS + 1 - NUM_PLAYLIST_SEGMENTS
segments = []
- for sequence in range(start, MAX_SEGMENTS + 2):
+ for sequence in range(start, MAX_SEGMENTS + 1):
segments.append(make_segment(sequence))
assert await resp.text() == make_playlist(
sequence=start,
diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py
index 9097d03a7a9..31661db3886 100644
--- a/tests/components/stream/test_recorder.py
+++ b/tests/components/stream/test_recorder.py
@@ -1,87 +1,27 @@
"""The tests for hls streams."""
-from __future__ import annotations
-
-import asyncio
-from collections import deque
from datetime import timedelta
from io import BytesIO
-import logging
import os
-import threading
from unittest.mock import patch
-import async_timeout
import av
import pytest
from homeassistant.components.stream import create_stream
-from homeassistant.components.stream.core import Segment
-from homeassistant.components.stream.fmp4utils import get_init_and_moof_data
+from homeassistant.components.stream.const import HLS_PROVIDER, RECORDER_PROVIDER
+from homeassistant.components.stream.core import Part, Segment
+from homeassistant.components.stream.fmp4utils import find_box
from homeassistant.components.stream.recorder import recorder_save_worker
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed
-from tests.components.stream.common import generate_h264_video
+from tests.components.stream.common import generate_h264_video, remux_with_audio
-TEST_TIMEOUT = 7.0 # Lower than 9s home assistant timeout
MAX_ABORT_SEGMENTS = 20 # Abort test to avoid looping forever
-class SaveRecordWorkerSync:
- """
- Test fixture to manage RecordOutput thread for recorder_save_worker.
-
- This is used to assert that the worker is started and stopped cleanly
- to avoid thread leaks in tests.
- """
-
- def __init__(self):
- """Initialize SaveRecordWorkerSync."""
- self.reset()
- self._segments = None
- self._save_thread = None
-
- def recorder_save_worker(self, file_out: str, segments: deque[Segment]):
- """Mock method for patch."""
- logging.debug("recorder_save_worker thread started")
- assert self._save_thread is None
- self._segments = segments
- self._save_thread = threading.current_thread()
- self._save_event.set()
-
- async def get_segments(self):
- """Return the recorded video segments."""
- with async_timeout.timeout(TEST_TIMEOUT):
- await self._save_event.wait()
- return self._segments
-
- async def join(self):
- """Verify save worker was invoked and block on shutdown."""
- with async_timeout.timeout(TEST_TIMEOUT):
- await self._save_event.wait()
- self._save_thread.join(timeout=TEST_TIMEOUT)
- assert not self._save_thread.is_alive()
-
- def reset(self):
- """Reset callback state for reuse in tests."""
- self._save_thread = None
- self._save_event = asyncio.Event()
-
-
-@pytest.fixture()
-def record_worker_sync(hass):
- """Patch recorder_save_worker for clean thread shutdown for test."""
- sync = SaveRecordWorkerSync()
- with patch(
- "homeassistant.components.stream.recorder.recorder_save_worker",
- side_effect=sync.recorder_save_worker,
- autospec=True,
- ):
- yield sync
-
-
async def test_record_stream(hass, hass_client, record_worker_sync):
"""
Test record stream.
@@ -94,7 +34,7 @@ async def test_record_stream(hass, hass_client, record_worker_sync):
# Setup demo track
source = generate_h264_video()
- stream = create_stream(hass, source)
+ stream = create_stream(hass, source, {})
with patch.object(hass.config, "is_allowed_path", return_value=True):
await stream.async_record("/example/path")
@@ -116,10 +56,10 @@ async def test_record_lookback(
await async_setup_component(hass, "stream", {"stream": {}})
source = generate_h264_video()
- stream = create_stream(hass, source)
+ stream = create_stream(hass, source, {})
# Start an HLS feed to enable lookback
- stream.add_provider("hls")
+ stream.add_provider(HLS_PROVIDER)
stream.start()
with patch.object(hass.config, "is_allowed_path", return_value=True):
@@ -145,10 +85,10 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync):
# Setup demo track
source = generate_h264_video()
- stream = create_stream(hass, source)
+ stream = create_stream(hass, source, {})
with patch.object(hass.config, "is_allowed_path", return_value=True):
await stream.async_record("/example/path")
- recorder = stream.add_provider("recorder")
+ recorder = stream.add_provider(RECORDER_PROVIDER)
await recorder.recv()
@@ -171,13 +111,28 @@ async def test_record_path_not_allowed(hass, hass_client):
# Setup demo track
source = generate_h264_video()
- stream = create_stream(hass, source)
+ stream = create_stream(hass, source, {})
with patch.object(
hass.config, "is_allowed_path", return_value=False
), pytest.raises(HomeAssistantError):
await stream.async_record("/example/path")
+def add_parts_to_segment(segment, source):
+ """Add relevant part data to segment for testing recorder."""
+ moof_locs = list(find_box(source.getbuffer(), b"moof")) + [len(source.getbuffer())]
+ segment.init = source.getbuffer()[: moof_locs[0]].tobytes()
+ segment.parts = [
+ Part(
+ duration=None,
+ has_keyframe=None,
+ http_range_start=None,
+ data=source.getbuffer()[moof_locs[i] : moof_locs[i + 1]],
+ )
+ for i in range(1, len(moof_locs) - 1)
+ ]
+
+
async def test_recorder_save(tmpdir):
"""Test recorder save."""
# Setup
@@ -185,9 +140,10 @@ async def test_recorder_save(tmpdir):
filename = f"{tmpdir}/test.mp4"
# Run
- recorder_save_worker(
- filename, [Segment(1, *get_init_and_moof_data(source.getbuffer()), 4)]
- )
+ segment = Segment(sequence=1)
+ add_parts_to_segment(segment, source)
+ segment.duration = 4
+ recorder_save_worker(filename, [segment])
# Assert
assert os.path.exists(filename)
@@ -200,15 +156,13 @@ async def test_recorder_discontinuity(tmpdir):
filename = f"{tmpdir}/test.mp4"
# Run
- init, moof_data = get_init_and_moof_data(source.getbuffer())
- recorder_save_worker(
- filename,
- [
- Segment(1, init, moof_data, 4, 0),
- Segment(2, init, moof_data, 4, 1),
- ],
- )
-
+ segment_1 = Segment(sequence=1, stream_id=0)
+ add_parts_to_segment(segment_1, source)
+ segment_1.duration = 4
+ segment_2 = Segment(sequence=2, stream_id=1)
+ add_parts_to_segment(segment_2, source)
+ segment_2.duration = 4
+ recorder_save_worker(filename, [segment_1, segment_2])
# Assert
assert os.path.exists(filename)
@@ -236,33 +190,38 @@ async def test_record_stream_audio(
"""
await async_setup_component(hass, "stream", {"stream": {}})
+ # Generate source video with no audio
+ source = generate_h264_video(container_format="mov")
+
for a_codec, expected_audio_streams in (
("aac", 1), # aac is a valid mp4 codec
("pcm_mulaw", 0), # G.711 is not a valid mp4 codec
("empty", 0), # audio stream with no packets
(None, 0), # no audio stream
):
+
+ # Remux source video with new audio
+ source = remux_with_audio(source, "mov", a_codec) # mov can store PCM
+
record_worker_sync.reset()
stream_worker_sync.pause()
- # Setup demo track
- source = generate_h264_video(
- container_format="mov", audio_codec=a_codec
- ) # mov can store PCM
- stream = create_stream(hass, source)
+ stream = create_stream(hass, source, {})
with patch.object(hass.config, "is_allowed_path", return_value=True):
await stream.async_record("/example/path")
- recorder = stream.add_provider("recorder")
+ recorder = stream.add_provider(RECORDER_PROVIDER)
while True:
- segment = await recorder.recv()
- if not segment:
+ await recorder.recv()
+ if not (segment := recorder.last_segment):
break
last_segment = segment
stream_worker_sync.resume()
result = av.open(
- BytesIO(last_segment.init + last_segment.moof_data), "r", format="mp4"
+ BytesIO(last_segment.init + last_segment.get_bytes_without_init()),
+ "r",
+ format="mp4",
)
assert len(result.streams.audio) == expected_audio_streams
@@ -278,7 +237,7 @@ async def test_record_stream_audio(
async def test_recorder_log(hass, caplog):
"""Test starting a stream to record logs the url without username and password."""
await async_setup_component(hass, "stream", {"stream": {}})
- stream = create_stream(hass, "https://abcd:efgh@foo.bar")
+ stream = create_stream(hass, "https://abcd:efgh@foo.bar", {})
with patch.object(hass.config, "is_allowed_path", return_value=True):
await stream.async_record("/example/path")
assert "https://abcd:efgh@foo.bar" not in caplog.text
diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py
index d5527105a70..793038c6770 100644
--- a/tests/components/stream/test_worker.py
+++ b/tests/components/stream/test_worker.py
@@ -21,24 +21,28 @@ from unittest.mock import patch
import av
-from homeassistant.components.stream import Stream
+from homeassistant.components.stream import Stream, create_stream
from homeassistant.components.stream.const import (
+ HLS_PROVIDER,
MAX_MISSING_DTS,
- MIN_SEGMENT_DURATION,
PACKETS_TO_WAIT_FOR_AUDIO,
+ TARGET_SEGMENT_DURATION,
)
from homeassistant.components.stream.worker import SegmentBuffer, stream_worker
+from homeassistant.setup import async_setup_component
+
+from tests.components.stream.common import generate_h264_video
STREAM_SOURCE = "some-stream-source"
# Formats here are arbitrary, not exercised by tests
-STREAM_OUTPUT_FORMAT = "hls"
AUDIO_STREAM_FORMAT = "mp3"
VIDEO_STREAM_FORMAT = "h264"
VIDEO_FRAME_RATE = 12
AUDIO_SAMPLE_RATE = 11025
+KEYFRAME_INTERVAL = 1 # in seconds
PACKET_DURATION = fractions.Fraction(1, VIDEO_FRAME_RATE) # in seconds
SEGMENT_DURATION = (
- math.ceil(MIN_SEGMENT_DURATION / PACKET_DURATION) * PACKET_DURATION
+ math.ceil(TARGET_SEGMENT_DURATION / KEYFRAME_INTERVAL) * KEYFRAME_INTERVAL
) # in seconds
TEST_SEQUENCE_LENGTH = 5 * VIDEO_FRAME_RATE
LONGER_TEST_SEQUENCE_LENGTH = 20 * VIDEO_FRAME_RATE
@@ -98,11 +102,12 @@ class PacketSequence:
super().__init__(3)
time_base = fractions.Fraction(1, VIDEO_FRAME_RATE)
- dts = self.packet * PACKET_DURATION / time_base
- pts = self.packet * PACKET_DURATION / time_base
- duration = PACKET_DURATION / time_base
+ dts = int(self.packet * PACKET_DURATION / time_base)
+ pts = int(self.packet * PACKET_DURATION / time_base)
+ duration = int(PACKET_DURATION / time_base)
stream = VIDEO_STREAM
- is_keyframe = True
+ # Pretend we get 1 keyframe every second
+ is_keyframe = not (self.packet - 1) % (VIDEO_FRAME_RATE * KEYFRAME_INTERVAL)
size = 3
return FakePacket()
@@ -175,6 +180,11 @@ class FakePyAvBuffer:
"""Capture the output segment for tests to inspect."""
self.segments.append(segment)
+ @property
+ def complete_segments(self):
+ """Return only the complete segments."""
+ return [segment for segment in self.segments if segment.complete]
+
class MockPyAv:
"""Mocks out av.open."""
@@ -195,10 +205,23 @@ class MockPyAv:
return self.container
+class MockFlushPart:
+ """Class to hold a wrapper function for check_flush_part."""
+
+ # Wrap this method with a preceding write so the BytesIO pointer moves
+ check_flush_part = SegmentBuffer.check_flush_part
+
+ @classmethod
+ def wrapped_check_flush_part(cls, segment_buffer, packet):
+ """Wrap check_flush_part to also advance the memory_file pointer."""
+ segment_buffer._memory_file.write(b"0")
+ return cls.check_flush_part(segment_buffer, packet)
+
+
async def async_decode_stream(hass, packets, py_av=None):
"""Start a stream worker that decodes incoming stream packets into output segments."""
- stream = Stream(hass, STREAM_SOURCE)
- stream.add_provider(STREAM_OUTPUT_FORMAT)
+ stream = Stream(hass, STREAM_SOURCE, {})
+ stream.add_provider(HLS_PROVIDER)
if not py_av:
py_av = MockPyAv()
@@ -207,6 +230,10 @@ async def async_decode_stream(hass, packets, py_av=None):
with patch("av.open", new=py_av.open), patch(
"homeassistant.components.stream.core.StreamOutput.put",
side_effect=py_av.capture_buffer.capture_output_segment,
+ ), patch(
+ "homeassistant.components.stream.worker.SegmentBuffer.check_flush_part",
+ side_effect=MockFlushPart.wrapped_check_flush_part,
+ autospec=True,
):
segment_buffer = SegmentBuffer(stream.outputs)
stream_worker(STREAM_SOURCE, {}, segment_buffer, threading.Event())
@@ -217,8 +244,8 @@ async def async_decode_stream(hass, packets, py_av=None):
async def test_stream_open_fails(hass):
"""Test failure on stream open."""
- stream = Stream(hass, STREAM_SOURCE)
- stream.add_provider(STREAM_OUTPUT_FORMAT)
+ stream = Stream(hass, STREAM_SOURCE, {})
+ stream.add_provider(HLS_PROVIDER)
with patch("av.open") as av_open:
av_open.side_effect = av.error.InvalidDataError(-2, "error")
segment_buffer = SegmentBuffer(stream.outputs)
@@ -233,13 +260,16 @@ async def test_stream_worker_success(hass):
hass, PacketSequence(TEST_SEQUENCE_LENGTH)
)
segments = decoded_stream.segments
+ complete_segments = decoded_stream.complete_segments
# Check number of segments. A segment is only formed when a packet from the next
# segment arrives, hence the subtraction of one from the sequence length.
- assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET)
+ assert len(complete_segments) == int(
+ (TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET
+ )
# Check sequence numbers
- assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ assert all(segments[i].sequence == i for i in range(len(segments)))
# Check segment durations
- assert all([s.duration == SEGMENT_DURATION for s in segments])
+ assert all(s.duration == SEGMENT_DURATION for s in complete_segments)
assert len(decoded_stream.video_packets) == TEST_SEQUENCE_LENGTH
assert len(decoded_stream.audio_packets) == 0
@@ -247,33 +277,39 @@ async def test_stream_worker_success(hass):
async def test_skip_out_of_order_packet(hass):
"""Skip a single out of order packet."""
packets = list(PacketSequence(TEST_SEQUENCE_LENGTH))
+ # for this test, make sure the out of order index doesn't happen on a keyframe
+ out_of_order_index = OUT_OF_ORDER_PACKET_INDEX
+ if packets[out_of_order_index].is_keyframe:
+ out_of_order_index += 1
# This packet is out of order
- packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9090
+ assert not packets[out_of_order_index].is_keyframe
+ packets[out_of_order_index].dts = -9090
decoded_stream = await async_decode_stream(hass, iter(packets))
segments = decoded_stream.segments
+ complete_segments = decoded_stream.complete_segments
# Check sequence numbers
- assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ assert all(segments[i].sequence == i for i in range(len(segments)))
# If skipped packet would have been the first packet of a segment, the previous
# segment will be longer by a packet duration
# We also may possibly lose a segment due to the shifting pts boundary
- if OUT_OF_ORDER_PACKET_INDEX % PACKETS_PER_SEGMENT == 0:
+ if out_of_order_index % PACKETS_PER_SEGMENT == 0:
# Check duration of affected segment and remove it
- longer_segment_index = int(
- (OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET
- )
+ longer_segment_index = int((out_of_order_index - 1) * SEGMENTS_PER_PACKET)
assert (
segments[longer_segment_index].duration
== SEGMENT_DURATION + PACKET_DURATION
)
del segments[longer_segment_index]
# Check number of segments
- assert len(segments) == int((len(packets) - 1 - 1) * SEGMENTS_PER_PACKET - 1)
+ assert len(complete_segments) == int(
+ (len(packets) - 1 - 1) * SEGMENTS_PER_PACKET - 1
+ )
else: # Otherwise segment durations and number of segments are unaffected
# Check number of segments
- assert len(segments) == int((len(packets) - 1) * SEGMENTS_PER_PACKET)
+ assert len(complete_segments) == int((len(packets) - 1) * SEGMENTS_PER_PACKET)
# Check remaining segment durations
- assert all([s.duration == SEGMENT_DURATION for s in segments])
+ assert all(s.duration == SEGMENT_DURATION for s in complete_segments)
assert len(decoded_stream.video_packets) == len(packets) - 1
assert len(decoded_stream.audio_packets) == 0
@@ -287,12 +323,15 @@ async def test_discard_old_packets(hass):
decoded_stream = await async_decode_stream(hass, iter(packets))
segments = decoded_stream.segments
+ complete_segments = decoded_stream.complete_segments
# Check number of segments
- assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET)
+ assert len(complete_segments) == int(
+ (OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET
+ )
# Check sequence numbers
- assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ assert all(segments[i].sequence == i for i in range(len(segments)))
# Check segment durations
- assert all([s.duration == SEGMENT_DURATION for s in segments])
+ assert all(s.duration == SEGMENT_DURATION for s in complete_segments)
assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX
assert len(decoded_stream.audio_packets) == 0
@@ -306,12 +345,15 @@ async def test_packet_overflow(hass):
decoded_stream = await async_decode_stream(hass, iter(packets))
segments = decoded_stream.segments
+ complete_segments = decoded_stream.complete_segments
# Check number of segments
- assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET)
+ assert len(complete_segments) == int(
+ (OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET
+ )
# Check sequence numbers
- assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ assert all(segments[i].sequence == i for i in range(len(segments)))
# Check segment durations
- assert all([s.duration == SEGMENT_DURATION for s in segments])
+ assert all(s.duration == SEGMENT_DURATION for s in complete_segments)
assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX
assert len(decoded_stream.audio_packets) == 0
@@ -327,15 +369,22 @@ async def test_skip_initial_bad_packets(hass):
decoded_stream = await async_decode_stream(hass, iter(packets))
segments = decoded_stream.segments
- # Check number of segments
- assert len(segments) == int(
- (num_packets - num_bad_packets - 1) * SEGMENTS_PER_PACKET
- )
+ complete_segments = decoded_stream.complete_segments
# Check sequence numbers
- assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ assert all(segments[i].sequence == i for i in range(len(segments)))
# Check segment durations
- assert all([s.duration == SEGMENT_DURATION for s in segments])
- assert len(decoded_stream.video_packets) == num_packets - num_bad_packets
+ assert all(s.duration == SEGMENT_DURATION for s in complete_segments)
+ assert (
+ len(decoded_stream.video_packets)
+ == num_packets
+ - math.ceil(num_bad_packets / (VIDEO_FRAME_RATE * KEYFRAME_INTERVAL))
+ * VIDEO_FRAME_RATE
+ * KEYFRAME_INTERVAL
+ )
+ # Check number of segments
+ assert len(complete_segments) == int(
+ (len(decoded_stream.video_packets) - 1) * SEGMENTS_PER_PACKET
+ )
assert len(decoded_stream.audio_packets) == 0
@@ -363,17 +412,18 @@ async def test_skip_missing_dts(hass):
bad_packet_start = int(LONGER_TEST_SEQUENCE_LENGTH / 2)
num_bad_packets = MAX_MISSING_DTS - 1
for i in range(bad_packet_start, bad_packet_start + num_bad_packets):
+ if packets[i].is_keyframe:
+ num_bad_packets -= 1
+ continue
packets[i].dts = None
decoded_stream = await async_decode_stream(hass, iter(packets))
segments = decoded_stream.segments
+ complete_segments = decoded_stream.complete_segments
# Check sequence numbers
- assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
- # Check segment durations (not counting the elongated segment)
- assert (
- sum([segments[i].duration == SEGMENT_DURATION for i in range(len(segments))])
- >= len(segments) - 1
- )
+ assert all(segments[i].sequence == i for i in range(len(segments)))
+ # Check segment durations (not counting the last segment)
+ assert sum(segment.duration for segment in complete_segments) >= len(segments) - 1
assert len(decoded_stream.video_packets) == num_packets - num_bad_packets
assert len(decoded_stream.audio_packets) == 0
@@ -389,8 +439,8 @@ async def test_too_many_bad_packets(hass):
packets[i].dts = None
decoded_stream = await async_decode_stream(hass, iter(packets))
- segments = decoded_stream.segments
- assert len(segments) == int((bad_packet_start - 1) * SEGMENTS_PER_PACKET)
+ complete_segments = decoded_stream.complete_segments
+ assert len(complete_segments) == int((bad_packet_start - 1) * SEGMENTS_PER_PACKET)
assert len(decoded_stream.video_packets) == bad_packet_start
assert len(decoded_stream.audio_packets) == 0
@@ -417,8 +467,8 @@ async def test_audio_packets_not_found(hass):
packets = PacketSequence(num_packets) # Contains only video packets
decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av)
- segments = decoded_stream.segments
- assert len(segments) == int((num_packets - 1) * SEGMENTS_PER_PACKET)
+ complete_segments = decoded_stream.complete_segments
+ assert len(complete_segments) == int((num_packets - 1) * SEGMENTS_PER_PACKET)
assert len(decoded_stream.video_packets) == num_packets
assert len(decoded_stream.audio_packets) == 0
@@ -430,8 +480,8 @@ async def test_adts_aac_audio(hass):
num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1
packets = list(PacketSequence(num_packets))
packets[1].stream = AUDIO_STREAM
- packets[1].dts = packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
- packets[1].pts = packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
+ packets[1].dts = int(packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE)
+ packets[1].pts = int(packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE)
# The following is packet data is a sign of ADTS AAC
packets[1][0] = 255
packets[1][1] = 241
@@ -448,16 +498,17 @@ async def test_audio_is_first_packet(hass):
packets = list(PacketSequence(num_packets))
# Pair up an audio packet for each video packet
packets[0].stream = AUDIO_STREAM
- packets[0].dts = packets[1].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
- packets[0].pts = packets[1].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
+ packets[0].dts = int(packets[1].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE)
+ packets[0].pts = int(packets[1].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE)
+ packets[1].is_keyframe = True # Move the video keyframe from packet 0 to packet 1
packets[2].stream = AUDIO_STREAM
- packets[2].dts = packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
- packets[2].pts = packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
+ packets[2].dts = int(packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE)
+ packets[2].pts = int(packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE)
decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av)
- segments = decoded_stream.segments
+ complete_segments = decoded_stream.complete_segments
# The audio packets are segmented with the video packets
- assert len(segments) == int((num_packets - 2 - 1) * SEGMENTS_PER_PACKET)
+ assert len(complete_segments) == int((num_packets - 2 - 1) * SEGMENTS_PER_PACKET)
assert len(decoded_stream.video_packets) == num_packets - 2
assert len(decoded_stream.audio_packets) == 1
@@ -469,13 +520,13 @@ async def test_audio_packets_found(hass):
num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1
packets = list(PacketSequence(num_packets))
packets[1].stream = AUDIO_STREAM
- packets[1].dts = packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
- packets[1].pts = packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
+ packets[1].dts = int(packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE)
+ packets[1].pts = int(packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE)
decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av)
- segments = decoded_stream.segments
+ complete_segments = decoded_stream.complete_segments
# The audio packet above is buffered with the video packet
- assert len(segments) == int((num_packets - 1 - 1) * SEGMENTS_PER_PACKET)
+ assert len(complete_segments) == int((num_packets - 1 - 1) * SEGMENTS_PER_PACKET)
assert len(decoded_stream.video_packets) == num_packets - 1
assert len(decoded_stream.audio_packets) == 1
@@ -492,12 +543,15 @@ async def test_pts_out_of_order(hass):
decoded_stream = await async_decode_stream(hass, iter(packets))
segments = decoded_stream.segments
+ complete_segments = decoded_stream.complete_segments
# Check number of segments
- assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET)
+ assert len(complete_segments) == int(
+ (TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET
+ )
# Check sequence numbers
- assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ assert all(segments[i].sequence == i for i in range(len(segments)))
# Check segment durations
- assert all([s.duration == SEGMENT_DURATION for s in segments])
+ assert all(s.duration == SEGMENT_DURATION for s in complete_segments)
assert len(decoded_stream.video_packets) == len(packets)
assert len(decoded_stream.audio_packets) == 0
@@ -511,8 +565,8 @@ async def test_stream_stopped_while_decoding(hass):
worker_open = threading.Event()
worker_wake = threading.Event()
- stream = Stream(hass, STREAM_SOURCE)
- stream.add_provider(STREAM_OUTPUT_FORMAT)
+ stream = Stream(hass, STREAM_SOURCE, {})
+ stream.add_provider(HLS_PROVIDER)
py_av = MockPyAv()
py_av.container.packets = PacketSequence(TEST_SEQUENCE_LENGTH)
@@ -538,8 +592,8 @@ async def test_update_stream_source(hass):
worker_open = threading.Event()
worker_wake = threading.Event()
- stream = Stream(hass, STREAM_SOURCE)
- stream.add_provider(STREAM_OUTPUT_FORMAT)
+ stream = Stream(hass, STREAM_SOURCE, {})
+ stream.add_provider(HLS_PROVIDER)
# Note that keepalive is not set here. The stream is "restarted" even though
# it is not stopping due to failure.
@@ -558,7 +612,11 @@ async def test_update_stream_source(hass):
worker_wake.wait()
return py_av.open(stream_source, args, kwargs)
- with patch("av.open", new=blocking_open):
+ with patch("av.open", new=blocking_open), patch(
+ "homeassistant.components.stream.worker.SegmentBuffer.check_flush_part",
+ side_effect=MockFlushPart.wrapped_check_flush_part,
+ autospec=True,
+ ):
stream.start()
assert worker_open.wait(TIMEOUT)
assert last_stream_source == STREAM_SOURCE
@@ -572,14 +630,14 @@ async def test_update_stream_source(hass):
assert last_stream_source == STREAM_SOURCE + "-updated-source"
worker_wake.set()
- # Ccleanup
+ # Cleanup
stream.stop()
async def test_worker_log(hass, caplog):
"""Test that the worker logs the url without username and password."""
- stream = Stream(hass, "https://abcd:efgh@foo.bar")
- stream.add_provider(STREAM_OUTPUT_FORMAT)
+ stream = Stream(hass, "https://abcd:efgh@foo.bar", {})
+ stream.add_provider(HLS_PROVIDER)
with patch("av.open") as av_open:
av_open.side_effect = av.error.InvalidDataError(-2, "error")
segment_buffer = SegmentBuffer(stream.outputs)
@@ -589,3 +647,74 @@ async def test_worker_log(hass, caplog):
await hass.async_block_till_done()
assert "https://abcd:efgh@foo.bar" not in caplog.text
assert "https://****:****@foo.bar" in caplog.text
+
+
+async def test_durations(hass, record_worker_sync):
+ """Test that the duration metadata matches the media."""
+ await async_setup_component(hass, "stream", {"stream": {}})
+
+ source = generate_h264_video()
+ stream = create_stream(hass, source, {})
+
+ # use record_worker_sync to grab output segments
+ with patch.object(hass.config, "is_allowed_path", return_value=True):
+ await stream.async_record("/example/path")
+
+ complete_segments = list(await record_worker_sync.get_segments())[:-1]
+ assert len(complete_segments) >= 1
+
+ # check that the Part duration metadata matches the durations in the media
+ running_metadata_duration = 0
+ for segment in complete_segments:
+ for part in segment.parts:
+ av_part = av.open(io.BytesIO(segment.init + part.data))
+ running_metadata_duration += part.duration
+ # av_part.duration will just return the largest dts in av_part.
+ # When we normalize by av.time_base this should equal the running duration
+ assert math.isclose(
+ running_metadata_duration,
+ av_part.duration / av.time_base,
+ abs_tol=1e-6,
+ )
+ av_part.close()
+ # check that the Part durations are consistent with the Segment durations
+ for segment in complete_segments:
+ assert math.isclose(
+ sum(part.duration for part in segment.parts), segment.duration, abs_tol=1e-6
+ )
+
+ await record_worker_sync.join()
+
+ stream.stop()
+
+
+async def test_has_keyframe(hass, record_worker_sync):
+ """Test that the has_keyframe metadata matches the media."""
+ await async_setup_component(hass, "stream", {"stream": {}})
+
+ source = generate_h264_video()
+ stream = create_stream(hass, source, {})
+
+ # use record_worker_sync to grab output segments
+ with patch.object(hass.config, "is_allowed_path", return_value=True):
+ await stream.async_record("/example/path")
+
+ # Our test video has keyframes every second. Use smaller parts so we have more
+ # part boundaries to better test keyframe logic.
+ with patch("homeassistant.components.stream.worker.TARGET_PART_DURATION", 0.25):
+ complete_segments = list(await record_worker_sync.get_segments())[:-1]
+ assert len(complete_segments) >= 1
+
+ # check that the Part has_keyframe metadata matches the keyframes in the media
+ for segment in complete_segments:
+ for part in segment.parts:
+ av_part = av.open(io.BytesIO(segment.init + part.data))
+ media_has_keyframe = any(
+ packet.is_keyframe for packet in av_part.demux(av_part.streams.video[0])
+ )
+ av_part.close()
+ assert part.has_keyframe == media_has_keyframe
+
+ await record_worker_sync.join()
+
+ stream.stop()
diff --git a/tests/components/syncthing/test_config_flow.py b/tests/components/syncthing/test_config_flow.py
index 30f8bc0386b..7cdf728c07f 100644
--- a/tests/components/syncthing/test_config_flow.py
+++ b/tests/components/syncthing/test_config_flow.py
@@ -34,7 +34,7 @@ async def test_show_setup_form(hass):
assert result["step_id"] == "user"
-async def test_flow_successfull(hass):
+async def test_flow_successful(hass):
"""Test with required fields only."""
with patch(
"aiosyncthing.system.System.status", return_value={"myID": "server-id"}
diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py
index b74799d1d12..e1ba2615742 100644
--- a/tests/components/tasmota/test_light.py
+++ b/tests/components/tasmota/test_light.py
@@ -189,8 +189,8 @@ async def test_attributes_rgb(hass, mqtt_mock, setup_tasmota):
state.attributes.get("supported_features")
== SUPPORT_EFFECT | SUPPORT_TRANSITION
)
- assert state.attributes.get("supported_color_modes") == ["rgb"]
- assert state.attributes.get("color_mode") == "rgb"
+ assert state.attributes.get("supported_color_modes") == ["hs"]
+ assert state.attributes.get("color_mode") == "hs"
async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota):
@@ -223,8 +223,8 @@ async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota):
state.attributes.get("supported_features")
== SUPPORT_EFFECT | SUPPORT_TRANSITION
)
- assert state.attributes.get("supported_color_modes") == ["rgb", "rgbw"]
- assert state.attributes.get("color_mode") == "rgbw"
+ assert state.attributes.get("supported_color_modes") == ["hs", "white"]
+ assert state.attributes.get("color_mode") == "hs"
async def test_attributes_rgbww(hass, mqtt_mock, setup_tasmota):
@@ -257,7 +257,7 @@ async def test_attributes_rgbww(hass, mqtt_mock, setup_tasmota):
state.attributes.get("supported_features")
== SUPPORT_EFFECT | SUPPORT_TRANSITION
)
- assert state.attributes.get("supported_color_modes") == ["color_temp", "rgb"]
+ assert state.attributes.get("supported_color_modes") == ["color_temp", "hs"]
assert state.attributes.get("color_mode") == "color_temp"
@@ -292,7 +292,7 @@ async def test_attributes_rgbww_reduced(hass, mqtt_mock, setup_tasmota):
state.attributes.get("supported_features")
== SUPPORT_EFFECT | SUPPORT_TRANSITION
)
- assert state.attributes.get("supported_color_modes") == ["color_temp", "rgb"]
+ assert state.attributes.get("supported_color_modes") == ["color_temp", "hs"]
assert state.attributes.get("color_mode") == "color_temp"
@@ -434,7 +434,7 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota):
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}')
state = hass.states.get("light.test")
assert state.state == STATE_ON
- assert state.attributes.get("color_mode") == "rgbw"
+ assert state.attributes.get("color_mode") == "hs"
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}')
state = hass.states.get("light.test")
@@ -442,24 +442,31 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota):
assert "color_mode" not in state.attributes
async_fire_mqtt_message(
- hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}'
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"White":0}'
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
assert state.attributes.get("brightness") == 127.5
- assert state.attributes.get("color_mode") == "rgbw"
+ assert state.attributes.get("color_mode") == "hs"
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":75,"White":75}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("brightness") == 191.25
+ assert state.attributes.get("color_mode") == "white"
async_fire_mqtt_message(
hass,
"tasmota_49A3BC/tele/STATE",
- '{"POWER":"ON","Color":"128,64,0","White":0}',
+ '{"POWER":"ON","Dimmer":50,"HSBColor":"30,100,50","White":0}',
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
assert state.attributes.get("brightness") == 127.5
- assert state.attributes.get("rgb_color") == (255, 128, 0)
- assert state.attributes.get("rgbw_color") == (255, 128, 0, 0)
- assert state.attributes.get("color_mode") == "rgbw"
+ assert state.attributes.get("hs_color") == (30, 100)
+ assert state.attributes.get("color_mode") == "hs"
async_fire_mqtt_message(
hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}'
@@ -467,9 +474,8 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota):
state = hass.states.get("light.test")
assert state.state == STATE_ON
assert state.attributes.get("brightness") == 127.5
- assert state.attributes.get("rgb_color") == (255, 192, 128)
- assert state.attributes.get("rgbw_color") == (255, 128, 0, 255)
- assert state.attributes.get("color_mode") == "rgbw"
+ assert state.attributes.get("rgb_color") is None
+ assert state.attributes.get("color_mode") == "white"
async_fire_mqtt_message(
hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":0}'
@@ -477,9 +483,8 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota):
state = hass.states.get("light.test")
assert state.state == STATE_ON
assert state.attributes.get("brightness") == 0
- assert state.attributes.get("rgb_color") == (0, 0, 0)
- assert state.attributes.get("rgbw_color") == (0, 0, 0, 0)
- assert state.attributes.get("color_mode") == "rgbw"
+ assert state.attributes.get("rgb_color") is None
+ assert state.attributes.get("color_mode") == "white"
async_fire_mqtt_message(
hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}'
@@ -545,12 +550,12 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota):
async_fire_mqtt_message(
hass,
"tasmota_49A3BC/tele/STATE",
- '{"POWER":"ON","Color":"128,64,0","White":0}',
+ '{"POWER":"ON","Dimmer":50,"HSBColor":"30,100,50","White":0}',
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
- assert state.attributes.get("rgb_color") == (255, 128, 0)
- assert state.attributes.get("color_mode") == "rgb"
+ assert state.attributes.get("hs_color") == (30, 100)
+ assert state.attributes.get("color_mode") == "hs"
async_fire_mqtt_message(
hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}'
@@ -578,114 +583,8 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota):
# Setting white to 0 should clear the color_temp
assert "white_value" not in state.attributes
assert "color_temp" not in state.attributes
- assert state.attributes.get("rgb_color") == (255, 128, 0)
- assert state.attributes.get("color_mode") == "rgb"
-
- async_fire_mqtt_message(
- hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}'
- )
- state = hass.states.get("light.test")
- assert state.state == STATE_ON
- assert state.attributes.get("effect") == "Cycle down"
-
- async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}')
-
- state = hass.states.get("light.test")
- assert state.state == STATE_ON
-
- async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}')
-
- state = hass.states.get("light.test")
- assert state.state == STATE_OFF
-
-
-async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmota):
- """Test state update via MQTT."""
- config = copy.deepcopy(DEFAULT_CONFIG)
- config["rl"][0] = 2
- config["lt_st"] = 5 # 5 channel light (RGBCW)
- config["so"]["17"] = 0 # Hex color in state updates
- mac = config["mac"]
-
- async_fire_mqtt_message(
- hass,
- f"{DEFAULT_PREFIX}/{mac}/config",
- json.dumps(config),
- )
- await hass.async_block_till_done()
-
- state = hass.states.get("light.test")
- assert state.state == "unavailable"
- assert not state.attributes.get(ATTR_ASSUMED_STATE)
- assert "color_mode" not in state.attributes
-
- async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
- state = hass.states.get("light.test")
- assert state.state == STATE_OFF
- assert not state.attributes.get(ATTR_ASSUMED_STATE)
- assert "color_mode" not in state.attributes
-
- async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}')
- state = hass.states.get("light.test")
- assert state.state == STATE_ON
- assert state.attributes.get("color_mode") == "color_temp"
-
- async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}')
- state = hass.states.get("light.test")
- assert state.state == STATE_OFF
- assert "color_mode" not in state.attributes
-
- async_fire_mqtt_message(
- hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}'
- )
- state = hass.states.get("light.test")
- assert state.state == STATE_ON
- assert state.attributes.get("brightness") == 127.5
- assert state.attributes.get("color_mode") == "color_temp"
-
- async_fire_mqtt_message(
- hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"804000","White":0}'
- )
- state = hass.states.get("light.test")
- assert state.state == STATE_ON
- assert state.attributes.get("rgb_color") == (255, 128, 0)
- assert state.attributes.get("color_mode") == "rgb"
-
- async_fire_mqtt_message(
- hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"0080400000"}'
- )
- state = hass.states.get("light.test")
- assert state.state == STATE_ON
- assert state.attributes.get("rgb_color") == (0, 255, 128)
- assert state.attributes.get("color_mode") == "rgb"
-
- async_fire_mqtt_message(
- hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}'
- )
- state = hass.states.get("light.test")
- assert state.state == STATE_ON
- assert "white_value" not in state.attributes
- # Setting white > 0 should clear the color
- assert "rgb_color" not in state.attributes
- assert state.attributes.get("color_mode") == "color_temp"
-
- async_fire_mqtt_message(
- hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}'
- )
- state = hass.states.get("light.test")
- assert state.state == STATE_ON
- assert state.attributes.get("color_temp") == 300
- assert state.attributes.get("color_mode") == "color_temp"
-
- async_fire_mqtt_message(
- hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}'
- )
- state = hass.states.get("light.test")
- assert state.state == STATE_ON
- # Setting white to 0 should clear the white_value and color_temp
- assert not state.attributes.get("white_value")
- assert not state.attributes.get("color_temp")
- assert state.attributes.get("color_mode") == "rgb"
+ assert state.attributes.get("hs_color") == (30, 100)
+ assert state.attributes.get("color_mode") == "hs"
async_fire_mqtt_message(
hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}'
@@ -752,12 +651,12 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm
async_fire_mqtt_message(
hass,
"tasmota_49A3BC/tele/STATE",
- '{"POWER":"ON","Color":"128,64,0","White":0}',
+ '{"POWER":"ON","HSBColor":"30,100,0","White":0}',
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
- assert state.attributes.get("rgb_color") == (255, 128, 0)
- assert state.attributes.get("color_mode") == "rgb"
+ assert state.attributes.get("hs_color") == (30, 100)
+ assert state.attributes.get("color_mode") == "hs"
async_fire_mqtt_message(
hass,
@@ -766,8 +665,8 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
- assert state.attributes.get("rgb_color") == (0, 0, 0)
- assert state.attributes.get("color_mode") == "rgb"
+ assert state.attributes.get("hs_color") == (30, 100)
+ assert state.attributes.get("color_mode") == "hs"
async_fire_mqtt_message(
hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"White":50}'
@@ -795,7 +694,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm
# Setting white to 0 should clear the white_value and color_temp
assert not state.attributes.get("white_value")
assert not state.attributes.get("color_temp")
- assert state.attributes.get("color_mode") == "rgb"
+ assert state.attributes.get("color_mode") == "hs"
async_fire_mqtt_message(
hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}'
@@ -950,27 +849,17 @@ async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota)
mqtt_mock.async_publish.reset_mock()
# Set color when setting color
- await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 32])
+ await common.async_turn_on(hass, "light.test", hs_color=[0, 100])
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Power1 ON;NoDelay;Color2 128,64,32",
+ "NoDelay;Power1 ON;NoDelay;HsbColor1 0;NoDelay;HsbColor2 100",
0,
False,
)
mqtt_mock.async_publish.reset_mock()
- # Set color when setting white is off
- await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0])
- mqtt_mock.async_publish.assert_called_once_with(
- "tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Power1 ON;NoDelay;Color2 128,64,32",
- 0,
- False,
- )
- mqtt_mock.async_publish.reset_mock()
-
- # Set white when white is on
- await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128])
+ # Set white when setting white
+ await common.async_turn_on(hass, "light.test", white=128)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
"NoDelay;Power1 ON;NoDelay;White 50",
@@ -979,6 +868,26 @@ async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota)
)
mqtt_mock.async_publish.reset_mock()
+ # rgbw_color should be ignored
+ await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0])
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/Backlog",
+ "NoDelay;Power1 ON",
+ 0,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # rgbw_color should be ignored
+ await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128])
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/Backlog",
+ "NoDelay;Power1 ON",
+ 0,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
await common.async_turn_on(hass, "light.test", white_value=128)
# white_value should be ignored
mqtt_mock.async_publish.assert_called_once_with(
@@ -1041,35 +950,45 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota):
# Turn the light on and verify MQTT messages are sent
await common.async_turn_on(hass, "light.test", brightness=192)
mqtt_mock.async_publish.assert_called_once_with(
- "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer4 75", 0, False
+ "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False
)
mqtt_mock.async_publish.reset_mock()
# Set color when setting color
- await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 32])
+ await common.async_turn_on(hass, "light.test", hs_color=[180, 50])
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Power1 ON;NoDelay;Color2 128,64,32",
+ "NoDelay;Power1 ON;NoDelay;HsbColor1 180;NoDelay;HsbColor2 50",
0,
False,
)
mqtt_mock.async_publish.reset_mock()
- # Set color when setting white is off
+ # Set white when setting white
+ await common.async_turn_on(hass, "light.test", white=128)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/Backlog",
+ "NoDelay;Power1 ON;NoDelay;White 50",
+ 0,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # rgbw_color should be ignored
await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0])
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Power1 ON;NoDelay;Color2 128,64,32,0",
+ "NoDelay;Power1 ON",
0,
False,
)
mqtt_mock.async_publish.reset_mock()
- # Set white when white is on
+ # rgbw_color should be ignored
await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128])
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Power1 ON;NoDelay;Color2 16,64,32,128",
+ "NoDelay;Power1 ON",
0,
False,
)
@@ -1141,10 +1060,10 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota):
)
mqtt_mock.async_publish.reset_mock()
- await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 32])
+ await common.async_turn_on(hass, "light.test", hs_color=[240, 75])
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Power1 ON;NoDelay;Color2 128,64,32",
+ "NoDelay;Power1 ON;NoDelay;HsbColor1 240;NoDelay;HsbColor2 75",
0,
False,
)
@@ -1331,7 +1250,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
async_fire_mqtt_message(
hass,
"tasmota_49A3BC/tele/STATE",
- '{"POWER":"ON","Dimmer":50, "Color":"0,255,0", "White":0}',
+ '{"POWER":"ON","Dimmer":50, "Color":"0,255,0","HSBColor":"120,100,50","White":0}',
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
@@ -1342,7 +1261,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 ON;NoDelay;Color2 255,0,0",
+ "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 ON;NoDelay;HsbColor1 0;NoDelay;HsbColor2 100",
0,
False,
)
@@ -1352,7 +1271,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
async_fire_mqtt_message(
hass,
"tasmota_49A3BC/tele/STATE",
- '{"POWER":"ON","Dimmer":100, "Color":"0,255,0"}',
+ '{"POWER":"ON","Dimmer":100, "Color":"0,255,0","HSBColor":"120,100,50"}',
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
@@ -1363,7 +1282,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade2 1;NoDelay;Speed2 12;NoDelay;Power1 ON;NoDelay;Color2 255,0,0",
+ "NoDelay;Fade2 1;NoDelay;Speed2 12;NoDelay;Power1 ON;NoDelay;HsbColor1 0;NoDelay;HsbColor2 100",
0,
False,
)
@@ -1668,7 +1587,7 @@ async def test_discovery_update_reconfigure_light(
state.attributes.get("supported_features")
== SUPPORT_EFFECT | SUPPORT_TRANSITION
)
- assert state.attributes.get("supported_color_modes") == ["rgb"]
+ assert state.attributes.get("supported_color_modes") == ["hs"]
async def test_availability_when_connection_lost(
diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py
index e9aa291fe6d..fc1e7fd624b 100644
--- a/tests/components/tasmota/test_sensor.py
+++ b/tests/components/tasmota/test_sensor.py
@@ -342,7 +342,7 @@ async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota)
state = hass.states.get("sensor.tasmota_energy_apparentpower_1")
assert state.state == "9.0"
state = hass.states.get("sensor.tasmota_energy_apparentpower_2")
- assert state.state == STATE_UNKNOWN
+ assert state.state == "5.6"
async_fire_mqtt_message(
hass, "tasmota_49A3BC/tele/SENSOR", '{"ENERGY":{"ApparentPower":2.3}}'
@@ -350,9 +350,9 @@ async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota)
state = hass.states.get("sensor.tasmota_energy_apparentpower_0")
assert state.state == "2.3"
state = hass.states.get("sensor.tasmota_energy_apparentpower_1")
- assert state.state == STATE_UNKNOWN
+ assert state.state == "9.0"
state = hass.states.get("sensor.tasmota_energy_apparentpower_2")
- assert state.state == STATE_UNKNOWN
+ assert state.state == "5.6"
# Test polled state update
async_fire_mqtt_message(
@@ -378,7 +378,7 @@ async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota)
state = hass.states.get("sensor.tasmota_energy_apparentpower_1")
assert state.state == "9.0"
state = hass.states.get("sensor.tasmota_energy_apparentpower_2")
- assert state.state == STATE_UNKNOWN
+ assert state.state == "5.6"
async_fire_mqtt_message(
hass,
@@ -388,9 +388,9 @@ async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota)
state = hass.states.get("sensor.tasmota_energy_apparentpower_0")
assert state.state == "2.3"
state = hass.states.get("sensor.tasmota_energy_apparentpower_1")
- assert state.state == STATE_UNKNOWN
+ assert state.state == "9.0"
state = hass.states.get("sensor.tasmota_energy_apparentpower_2")
- assert state.state == STATE_UNKNOWN
+ assert state.state == "5.6"
@pytest.mark.parametrize("status_sensor_disabled", [False])
diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py
index 4047a822432..df5c43aa58b 100644
--- a/tests/components/template/test_sensor.py
+++ b/tests/components/template/test_sensor.py
@@ -1044,6 +1044,7 @@ async def test_trigger_entity(hass):
"attributes": {
"plus_one": "{{ trigger.event.data.beer + 1 }}"
},
+ "state_class": "measurement",
}
],
},
@@ -1100,6 +1101,7 @@ async def test_trigger_entity(hass):
assert state.attributes.get("entity_picture") == "/local/dogs.png"
assert state.attributes.get("plus_one") == 3
assert state.attributes.get("unit_of_measurement") == "%"
+ assert state.attributes.get("state_class") == "measurement"
assert state.context is context
@@ -1167,3 +1169,31 @@ async def test_trigger_not_allowed_platform_config(hass, caplog):
"You can only add triggers to template entities if they are defined under `template:`."
in caplog.text
)
+
+
+async def test_config_top_level(hass):
+ """Test unique_id option only creates one sensor per id."""
+ await async_setup_component(
+ hass,
+ "template",
+ {
+ "template": {
+ "sensor": {
+ "name": "top-level",
+ "device_class": "battery",
+ "state_class": "measurement",
+ "state": "5",
+ "unit_of_measurement": "%",
+ },
+ },
+ },
+ )
+
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+ state = hass.states.get("sensor.top_level")
+ assert state is not None
+ assert state.state == "5"
+ assert state.attributes["device_class"] == "battery"
+ assert state.attributes["state_class"] == "measurement"
diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py
index af8c32a1549..b7c4a871068 100644
--- a/tests/components/threshold/test_binary_sensor.py
+++ b/tests/components/threshold/test_binary_sensor.py
@@ -1,6 +1,11 @@
"""The test for the threshold sensor platform."""
-from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+ TEMP_CELSIUS,
+)
from homeassistant.setup import async_setup_component
@@ -283,7 +288,7 @@ async def test_sensor_in_range_with_hysteresis(hass):
assert state.state == "on"
-async def test_sensor_in_range_unknown_state(hass):
+async def test_sensor_in_range_unknown_state(hass, caplog):
"""Test if source is within the range."""
config = {
"binary_sensor": {
@@ -322,6 +327,16 @@ async def test_sensor_in_range_unknown_state(hass):
assert state.attributes.get("position") == "unknown"
assert state.state == "off"
+ hass.states.async_set("sensor.test_monitored", STATE_UNAVAILABLE)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.threshold")
+
+ assert state.attributes.get("position") == "unknown"
+ assert state.state == "off"
+
+ assert "State is not numerical" not in caplog.text
+
async def test_sensor_lower_zero_threshold(hass):
"""Test if a lower threshold of zero is set."""
diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py
index 3751abfc361..7b80996db14 100644
--- a/tests/components/totalconnect/test_config_flow.py
+++ b/tests/components/totalconnect/test_config_flow.py
@@ -135,15 +135,14 @@ async def test_reauth(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data
)
- assert result["step_id"] == "reauth_confirm"
-
- result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth_confirm"
with patch(
"homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient"
- ) as client_mock:
+ ) as client_mock, patch(
+ "homeassistant.components.totalconnect.async_setup_entry", return_value=True
+ ):
# first test with an invalid password
client_mock.return_value.is_valid_credentials.return_value = False
@@ -162,5 +161,6 @@ async def test_reauth(hass):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "reauth_successful"
+ await hass.async_block_till_done()
assert len(hass.config_entries.async_entries()) == 1
diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py
index 79b341e4504..91dfa25fd35 100644
--- a/tests/components/transmission/test_config_flow.py
+++ b/tests/components/transmission/test_config_flow.py
@@ -296,7 +296,7 @@ async def test_error_on_connection_failure(hass, conn_error):
assert result["errors"] == {"base": "cannot_connect"}
-async def test_error_on_unknwon_error(hass, unknown_error):
+async def test_error_on_unknown_error(hass, unknown_error):
"""Test when connection to host fails."""
flow = init_config_flow(hass)
diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py
index 93f21911c78..6a911f8d4db 100644
--- a/tests/components/upnp/test_config_flow.py
+++ b/tests/components/upnp/test_config_flow.py
@@ -1,7 +1,7 @@
"""Test UPnP/IGD config flow."""
from datetime import timedelta
-from unittest.mock import AsyncMock, patch
+from unittest.mock import AsyncMock, Mock, patch
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import ssdp
@@ -35,6 +35,14 @@ async def test_flow_ssdp_discovery(hass: HomeAssistant):
udn = "uuid:device_1"
location = "dummy"
mock_device = MockDevice(udn)
+ ssdp_discoveries = [
+ {
+ ssdp.ATTR_SSDP_LOCATION: location,
+ ssdp.ATTR_SSDP_ST: mock_device.device_type,
+ ssdp.ATTR_UPNP_UDN: mock_device.udn,
+ ssdp.ATTR_SSDP_USN: mock_device.usn,
+ }
+ ]
discoveries = [
{
DISCOVERY_LOCATION: location,
@@ -49,7 +57,7 @@ async def test_flow_ssdp_discovery(hass: HomeAssistant):
with patch.object(
Device, "async_create_device", AsyncMock(return_value=mock_device)
), patch.object(
- Device, "async_discover", AsyncMock(return_value=discoveries)
+ ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries)
), patch.object(
Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0])
):
@@ -156,6 +164,14 @@ async def test_flow_user(hass: HomeAssistant):
udn = "uuid:device_1"
location = "dummy"
mock_device = MockDevice(udn)
+ ssdp_discoveries = [
+ {
+ ssdp.ATTR_SSDP_LOCATION: location,
+ ssdp.ATTR_SSDP_ST: mock_device.device_type,
+ ssdp.ATTR_UPNP_UDN: mock_device.udn,
+ ssdp.ATTR_SSDP_USN: mock_device.usn,
+ }
+ ]
discoveries = [
{
DISCOVERY_LOCATION: location,
@@ -171,7 +187,7 @@ async def test_flow_user(hass: HomeAssistant):
with patch.object(
Device, "async_create_device", AsyncMock(return_value=mock_device)
), patch.object(
- Device, "async_discover", AsyncMock(return_value=discoveries)
+ ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries)
), patch.object(
Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0])
):
@@ -202,6 +218,14 @@ async def test_flow_import(hass: HomeAssistant):
udn = "uuid:device_1"
mock_device = MockDevice(udn)
location = "dummy"
+ ssdp_discoveries = [
+ {
+ ssdp.ATTR_SSDP_LOCATION: location,
+ ssdp.ATTR_SSDP_ST: mock_device.device_type,
+ ssdp.ATTR_UPNP_UDN: mock_device.udn,
+ ssdp.ATTR_SSDP_USN: mock_device.usn,
+ }
+ ]
discoveries = [
{
DISCOVERY_LOCATION: location,
@@ -217,7 +241,7 @@ async def test_flow_import(hass: HomeAssistant):
with patch.object(
Device, "async_create_device", AsyncMock(return_value=mock_device)
), patch.object(
- Device, "async_discover", AsyncMock(return_value=discoveries)
+ ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries)
), patch.object(
Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0])
):
@@ -261,31 +285,19 @@ async def test_flow_import_already_configured(hass: HomeAssistant):
assert result["reason"] == "already_configured"
-async def test_flow_import_incomplete(hass: HomeAssistant):
- """Test config flow: incomplete discovery, configured through configuration.yaml."""
- udn = "uuid:device_1"
- mock_device = MockDevice(udn)
- location = "dummy"
- discoveries = [
- {
- DISCOVERY_LOCATION: location,
- DISCOVERY_NAME: mock_device.name,
- # DISCOVERY_ST: mock_device.device_type,
- DISCOVERY_UDN: mock_device.udn,
- DISCOVERY_UNIQUE_ID: mock_device.unique_id,
- DISCOVERY_USN: mock_device.usn,
- DISCOVERY_HOSTNAME: mock_device.hostname,
- }
- ]
-
- with patch.object(Device, "async_discover", AsyncMock(return_value=discoveries)):
+async def test_flow_import_no_devices_found(hass: HomeAssistant):
+ """Test config flow: no devices found, configured through configuration.yaml."""
+ ssdp_discoveries = []
+ with patch.object(
+ ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries)
+ ):
# Discovered via step import.
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "incomplete_discovery"
+ assert result["reason"] == "no_devices_found"
async def test_options_flow(hass: HomeAssistant):
@@ -294,15 +306,12 @@ async def test_options_flow(hass: HomeAssistant):
udn = "uuid:device_1"
location = "http://192.168.1.1/desc.xml"
mock_device = MockDevice(udn)
- discoveries = [
+ ssdp_discoveries = [
{
- DISCOVERY_LOCATION: location,
- DISCOVERY_NAME: mock_device.name,
- DISCOVERY_ST: mock_device.device_type,
- DISCOVERY_UDN: mock_device.udn,
- DISCOVERY_UNIQUE_ID: mock_device.unique_id,
- DISCOVERY_USN: mock_device.usn,
- DISCOVERY_HOSTNAME: mock_device.hostname,
+ ssdp.ATTR_SSDP_LOCATION: location,
+ ssdp.ATTR_SSDP_ST: mock_device.device_type,
+ ssdp.ATTR_UPNP_UDN: mock_device.udn,
+ ssdp.ATTR_SSDP_USN: mock_device.usn,
}
]
config_entry = MockConfigEntry(
@@ -321,7 +330,11 @@ async def test_options_flow(hass: HomeAssistant):
}
with patch.object(
Device, "async_create_device", AsyncMock(return_value=mock_device)
- ), patch.object(Device, "async_discover", AsyncMock(return_value=discoveries)):
+ ), patch.object(
+ ssdp,
+ "async_get_discovery_info_by_udn_st",
+ Mock(return_value=ssdp_discoveries[0]),
+ ):
# Initialisation of component.
await async_setup_component(hass, "upnp", config)
await hass.async_block_till_done()
diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py
index e6e37ca52fb..0770906f0da 100644
--- a/tests/components/upnp/test_init.py
+++ b/tests/components/upnp/test_init.py
@@ -1,17 +1,11 @@
"""Test UPnP/IGD setup process."""
-from unittest.mock import AsyncMock, patch
+from unittest.mock import AsyncMock, Mock, patch
+from homeassistant.components import ssdp
from homeassistant.components.upnp.const import (
CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN,
- DISCOVERY_HOSTNAME,
- DISCOVERY_LOCATION,
- DISCOVERY_NAME,
- DISCOVERY_ST,
- DISCOVERY_UDN,
- DISCOVERY_UNIQUE_ID,
- DISCOVERY_USN,
DOMAIN,
)
from homeassistant.components.upnp.device import Device
@@ -28,17 +22,12 @@ async def test_async_setup_entry_default(hass: HomeAssistant):
udn = "uuid:device_1"
location = "http://192.168.1.1/desc.xml"
mock_device = MockDevice(udn)
- discoveries = [
- {
- DISCOVERY_LOCATION: location,
- DISCOVERY_NAME: mock_device.name,
- DISCOVERY_ST: mock_device.device_type,
- DISCOVERY_UDN: mock_device.udn,
- DISCOVERY_UNIQUE_ID: mock_device.unique_id,
- DISCOVERY_USN: mock_device.usn,
- DISCOVERY_HOSTNAME: mock_device.hostname,
- }
- ]
+ discovery = {
+ ssdp.ATTR_SSDP_LOCATION: location,
+ ssdp.ATTR_SSDP_ST: mock_device.device_type,
+ ssdp.ATTR_UPNP_UDN: mock_device.udn,
+ ssdp.ATTR_SSDP_USN: mock_device.usn,
+ }
entry = MockConfigEntry(
domain=DOMAIN,
data={
@@ -51,77 +40,19 @@ async def test_async_setup_entry_default(hass: HomeAssistant):
# no upnp
}
async_create_device = AsyncMock(return_value=mock_device)
- async_discover = AsyncMock()
+ mock_get_discovery = Mock()
with patch.object(Device, "async_create_device", async_create_device), patch.object(
- Device, "async_discover", async_discover
+ ssdp, "async_get_discovery_info_by_udn_st", mock_get_discovery
):
# initialisation of component, no device discovered
- async_discover.return_value = []
+ mock_get_discovery.return_value = None
await async_setup_component(hass, "upnp", config)
await hass.async_block_till_done()
# loading of config_entry, device discovered
- async_discover.return_value = discoveries
+ mock_get_discovery.return_value = discovery
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id) is True
# ensure device is stored/used
- async_create_device.assert_called_with(hass, discoveries[0][DISCOVERY_LOCATION])
-
-
-async def test_sync_setup_entry_multiple_discoveries(hass: HomeAssistant):
- """Test async_setup_entry."""
- udn_0 = "uuid:device_1"
- location_0 = "http://192.168.1.1/desc.xml"
- mock_device_0 = MockDevice(udn_0)
- udn_1 = "uuid:device_2"
- location_1 = "http://192.168.1.2/desc.xml"
- mock_device_1 = MockDevice(udn_1)
- discoveries = [
- {
- DISCOVERY_LOCATION: location_0,
- DISCOVERY_NAME: mock_device_0.name,
- DISCOVERY_ST: mock_device_0.device_type,
- DISCOVERY_UDN: mock_device_0.udn,
- DISCOVERY_UNIQUE_ID: mock_device_0.unique_id,
- DISCOVERY_USN: mock_device_0.usn,
- DISCOVERY_HOSTNAME: mock_device_0.hostname,
- },
- {
- DISCOVERY_LOCATION: location_1,
- DISCOVERY_NAME: mock_device_1.name,
- DISCOVERY_ST: mock_device_1.device_type,
- DISCOVERY_UDN: mock_device_1.udn,
- DISCOVERY_UNIQUE_ID: mock_device_1.unique_id,
- DISCOVERY_USN: mock_device_1.usn,
- DISCOVERY_HOSTNAME: mock_device_1.hostname,
- },
- ]
- entry = MockConfigEntry(
- domain=DOMAIN,
- data={
- CONFIG_ENTRY_UDN: mock_device_1.udn,
- CONFIG_ENTRY_ST: mock_device_1.device_type,
- },
- )
-
- config = {
- # no upnp
- }
- async_create_device = AsyncMock(return_value=mock_device_1)
- async_discover = AsyncMock()
- with patch.object(Device, "async_create_device", async_create_device), patch.object(
- Device, "async_discover", async_discover
- ):
- # initialisation of component, no device discovered
- async_discover.return_value = []
- await async_setup_component(hass, "upnp", config)
- await hass.async_block_till_done()
-
- # loading of config_entry, device discovered
- async_discover.return_value = discoveries
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id) is True
-
- # ensure device is stored/used
- async_create_device.assert_called_with(hass, discoveries[1][DISCOVERY_LOCATION])
+ async_create_device.assert_called_with(hass, discovery[ssdp.ATTR_SSDP_LOCATION])
diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py
index ae3c0a1a1de..1ce55ac9e8f 100644
--- a/tests/components/vera/common.py
+++ b/tests/components/vera/common.py
@@ -65,7 +65,7 @@ def new_simple_controller_config(
setup_callback: SetupCallback = None,
legacy_entity_unique_id=False,
) -> ControllerConfig:
- """Create simple contorller config."""
+ """Create simple controller config."""
return ControllerConfig(
config=config or {CONF_CONTROLLER: "http://127.0.0.1:123"},
options=options,
diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py
index 35bf3cee242..21554cc4456 100644
--- a/tests/components/wallbox/__init__.py
+++ b/tests/components/wallbox/__init__.py
@@ -1 +1,44 @@
"""Tests for the Wallbox integration."""
+
+import json
+
+import requests_mock
+
+from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+
+from tests.common import MockConfigEntry
+
+test_response = json.loads(
+ '{"charging_power": 0,"max_available_power": "xx","charging_speed": 0,"added_range": "xx","added_energy": "44.697"}'
+)
+
+
+async def setup_integration(hass):
+ """Test wallbox sensor class setup."""
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_USERNAME: "test_username",
+ CONF_PASSWORD: "test_password",
+ CONF_STATION: "12345",
+ },
+ entry_id="testEntry",
+ )
+
+ entry.add_to_hass(hass)
+
+ with requests_mock.Mocker() as mock_request:
+ mock_request.get(
+ "https://api.wall-box.com/auth/token/user",
+ text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}',
+ status_code=200,
+ )
+ mock_request.get(
+ "https://api.wall-box.com/chargers/status/12345",
+ json=test_response,
+ status_code=200,
+ )
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py
index 074f67abe2c..6b5a05a3486 100644
--- a/tests/components/wallbox/test_config_flow.py
+++ b/tests/components/wallbox/test_config_flow.py
@@ -1,13 +1,18 @@
"""Test the Wallbox config flow."""
+import json
from unittest.mock import patch
-from voluptuous.schema_builder import raises
+import requests_mock
from homeassistant import config_entries, data_entry_flow
-from homeassistant.components.wallbox import CannotConnect, InvalidAuth, config_flow
+from homeassistant.components.wallbox import InvalidAuth, config_flow
from homeassistant.components.wallbox.const import DOMAIN
from homeassistant.core import HomeAssistant
+test_response = json.loads(
+ '{"charging_power": 0,"max_available_power": 25,"charging_speed": 0,"added_range": 372,"added_energy": 44.697}'
+)
+
async def test_show_set_form(hass: HomeAssistant) -> None:
"""Test that the setup form is served."""
@@ -42,16 +47,31 @@ async def test_form_invalid_auth(hass):
assert result2["errors"] == {"base": "invalid_auth"}
-async def test_form_cannot_connect(hass):
+async def test_form_cannot_authenticate(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch(
- "homeassistant.components.wallbox.config_flow.WallboxHub.async_authenticate",
- side_effect=CannotConnect,
- ):
+ with requests_mock.Mocker() as mock_request:
+ mock_request.get(
+ "https://api.wall-box.com/auth/token/user",
+ text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}',
+ status_code=403,
+ )
+ mock_request.get(
+ "https://api.wall-box.com/chargers/status/12345",
+ text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}',
+ status_code=403,
+ )
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "station": "12345",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@@ -65,64 +85,61 @@ async def test_form_cannot_connect(hass):
assert result2["errors"] == {"base": "invalid_auth"}
-async def test_validate_input(hass):
- """Test we can validate input."""
- data = {
- "station": "12345",
- "username": "test-username",
- "password": "test-password",
- }
+async def test_form_cannot_connect(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
- def alternate_authenticate_method():
- return None
+ with requests_mock.Mocker() as mock_request:
+ mock_request.get(
+ "https://api.wall-box.com/auth/token/user",
+ text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}',
+ status_code=200,
+ )
+ mock_request.get(
+ "https://api.wall-box.com/chargers/status/12345",
+ text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}',
+ status_code=404,
+ )
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "station": "12345",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
- def alternate_get_charger_status_method(station):
- data = '{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}'
- return data
-
- with patch(
- "wallbox.Wallbox.authenticate",
- side_effect=alternate_authenticate_method,
- ), patch(
- "wallbox.Wallbox.getChargerStatus",
- side_effect=alternate_get_charger_status_method,
- ):
-
- result = await config_flow.validate_input(hass, data)
-
- assert result == {"title": "Wallbox Portal"}
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "cannot_connect"}
-async def test_configflow_class():
- """Test configFlow class."""
- configflow = config_flow.ConfigFlow()
- assert configflow
+async def test_form_validate_input(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
- with patch(
- "homeassistant.components.wallbox.config_flow.validate_input",
- side_effect=TypeError,
- ), raises(Exception):
- assert await configflow.async_step_user(True)
+ with requests_mock.Mocker() as mock_request:
+ mock_request.get(
+ "https://api.wall-box.com/auth/token/user",
+ text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}',
+ status_code=200,
+ )
+ mock_request.get(
+ "https://api.wall-box.com/chargers/status/12345",
+ text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}',
+ status_code=200,
+ )
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "station": "12345",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
- with patch(
- "homeassistant.components.wallbox.config_flow.validate_input",
- side_effect=CannotConnect,
- ), raises(Exception):
- assert await configflow.async_step_user(True)
-
- with patch(
- "homeassistant.components.wallbox.config_flow.validate_input",
- ), raises(Exception):
- assert await configflow.async_step_user(True)
-
-
-def test_cannot_connect_class():
- """Test cannot Connect class."""
- cannot_connect = CannotConnect
- assert cannot_connect
-
-
-def test_invalid_auth_class():
- """Test invalid auth class."""
- invalid_auth = InvalidAuth
- assert invalid_auth
+ assert result2["title"] == "Wallbox Portal"
+ assert result2["data"]["station"] == "12345"
diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py
index 892e77dc7f6..874629bac3e 100644
--- a/tests/components/wallbox/test_init.py
+++ b/tests/components/wallbox/test_init.py
@@ -1,16 +1,12 @@
"""Test Wallbox Init Component."""
import json
-import pytest
-import requests_mock
-from voluptuous.schema_builder import raises
-
-from homeassistant.components import wallbox
from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
+from tests.components.wallbox import setup_integration
entry = MockConfigEntry(
domain=DOMAIN,
@@ -31,135 +27,9 @@ test_response_rounding_error = json.loads(
)
-async def test_wallbox_setup_entry(hass: HomeAssistantType):
- """Test Wallbox Setup."""
- with requests_mock.Mocker() as m:
- m.get(
- "https://api.wall-box.com/auth/token/user",
- text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}',
- status_code=200,
- )
- m.get(
- "https://api.wall-box.com/chargers/status/12345",
- text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}',
- status_code=200,
- )
- assert await wallbox.async_setup_entry(hass, entry)
-
- with requests_mock.Mocker() as m, raises(ConnectionError):
- m.get(
- "https://api.wall-box.com/auth/token/user",
- text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":404}',
- status_code=404,
- )
- assert await wallbox.async_setup_entry(hass, entry) is False
-
-
-async def test_wallbox_unload_entry(hass: HomeAssistantType):
+async def test_wallbox_unload_entry(hass: HomeAssistant):
"""Test Wallbox Unload."""
- hass.data[DOMAIN] = {"connections": {entry.entry_id: entry}}
- assert await wallbox.async_unload_entry(hass, entry)
+ await setup_integration(hass)
- hass.data[DOMAIN] = {"fail_entry": entry}
-
- with pytest.raises(KeyError):
- await wallbox.async_unload_entry(hass, entry)
-
-
-async def test_get_data(hass: HomeAssistantType):
- """Test hub class, get_data."""
-
- station = ("12345",)
- username = ("test-username",)
- password = "test-password"
-
- hub = wallbox.WallboxHub(station, username, password, hass)
-
- with requests_mock.Mocker() as m:
- m.get(
- "https://api.wall-box.com/auth/token/user",
- text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}',
- status_code=200,
- )
- m.get(
- "https://api.wall-box.com/chargers/status/('12345',)",
- json=test_response,
- status_code=200,
- )
- assert await hub.async_get_data()
-
-
-async def test_get_data_rounding_error(hass: HomeAssistantType):
- """Test hub class, get_data with rounding error."""
-
- station = ("12345",)
- username = ("test-username",)
- password = "test-password"
-
- hub = wallbox.WallboxHub(station, username, password, hass)
-
- with requests_mock.Mocker() as m:
- m.get(
- "https://api.wall-box.com/auth/token/user",
- text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}',
- status_code=200,
- )
- m.get(
- "https://api.wall-box.com/chargers/status/('12345',)",
- json=test_response_rounding_error,
- status_code=200,
- )
- assert await hub.async_get_data()
-
-
-async def test_authentication_exception(hass: HomeAssistantType):
- """Test hub class, authentication raises exception."""
-
- station = ("12345",)
- username = ("test-username",)
- password = "test-password"
-
- hub = wallbox.WallboxHub(station, username, password, hass)
-
- with requests_mock.Mocker() as m, raises(wallbox.InvalidAuth):
- m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=403)
-
- assert await hub.async_authenticate()
-
- with requests_mock.Mocker() as m, raises(ConnectionError):
- m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=404)
-
- assert await hub.async_authenticate()
-
- with requests_mock.Mocker() as m, raises(wallbox.InvalidAuth):
- m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=403)
- m.get(
- "https://api.wall-box.com/chargers/status/test",
- json=test_response,
- status_code=403,
- )
- assert await hub.async_get_data()
-
-
-async def test_get_data_exception(hass: HomeAssistantType):
- """Test hub class, authentication raises exception."""
-
- station = ("12345",)
- username = ("test-username",)
- password = "test-password"
-
- hub = wallbox.WallboxHub(station, username, password, hass)
-
- with requests_mock.Mocker() as m, raises(ConnectionError):
- m.get(
- "https://api.wall-box.com/auth/token/user",
- text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}',
- status_code=200,
- )
- m.get(
- "https://api.wall-box.com/chargers/status/('12345',)",
- text="data",
- status_code=404,
- )
- assert await hub.async_get_data()
+ assert await hass.config_entries.async_unload(entry.entry_id)
diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py
index 5c0c3511a30..b88ed094fda 100644
--- a/tests/components/wallbox/test_sensor.py
+++ b/tests/components/wallbox/test_sensor.py
@@ -1,13 +1,10 @@
"""Test Wallbox Switch component."""
-import json
-from unittest.mock import MagicMock
-
-from homeassistant.components.wallbox import sensor
from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from tests.common import MockConfigEntry
+from tests.components.wallbox import setup_integration
entry = MockConfigEntry(
domain=DOMAIN,
@@ -19,63 +16,17 @@ entry = MockConfigEntry(
entry_id="testEntry",
)
-test_response = json.loads(
- '{"charging_power": 0,"max_available_power": 25,"charging_speed": 0,"added_range": 372,"added_energy": 44.697}'
-)
-test_response_rounding_error = json.loads(
- '{"charging_power": "XX","max_available_power": "xx","charging_speed": 0,"added_range": "xx","added_energy": "XX"}'
-)
-
-CONF_STATION = ("12345",)
-CONF_USERNAME = ("test-username",)
-CONF_PASSWORD = "test-password"
-
-# wallbox = WallboxHub(CONF_STATION, CONF_USERNAME, CONF_PASSWORD, hass)
-
-
-async def test_wallbox_sensor_class():
+async def test_wallbox_sensor_class(hass):
"""Test wallbox sensor class."""
- coordinator = MagicMock(return_value="connected")
- idx = 1
- ent = "charging_power"
+ await setup_integration(hass)
- wallboxSensor = sensor.WallboxSensor(coordinator, idx, ent, entry)
+ state = hass.states.get("sensor.mock_title_charging_power")
+ assert state.attributes["unit_of_measurement"] == "kW"
+ assert state.attributes["icon"] == "mdi:ev-station"
+ assert state.name == "Mock Title Charging Power"
- assert wallboxSensor.icon == "mdi:ev-station"
- assert wallboxSensor.unit_of_measurement == "kW"
- assert wallboxSensor.name == "Mock Title Charging Power"
- assert wallboxSensor.state
-
-
-# async def test_wallbox_updater(hass: HomeAssistantType):
-# """Test wallbox updater."""
-# with requests_mock.Mocker() as m:
-# m.get(
-# "https://api.wall-box.com/auth/token/user",
-# text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}',
-# status_code=200,
-# )
-# m.get(
-# "https://api.wall-box.com/chargers/status/('12345',)",
-# json=test_response,
-# status_code=200,
-# )
-# await sensor.wallbox_updater(wallbox, hass)
-
-
-# async def test_wallbox_updater_rounding_error(hass: HomeAssistantType):
-# """Test wallbox updater rounding error."""
-# with requests_mock.Mocker() as m:
-# m.get(
-# "https://api.wall-box.com/auth/token/user",
-# text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}',
-# status_code=200,
-# )
-# m.get(
-# "https://api.wall-box.com/chargers/status/('12345',)",
-# json=test_response_rounding_error,
-# status_code=200,
-# )
-# await sensor.wallbox_updater(wallbox, hass)
+ state = hass.states.get("sensor.mock_title_charging_speed")
+ assert state.attributes["icon"] == "mdi:speedometer"
+ assert state.name == "Mock Title Charging Speed"
diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py
index 69b4b84dcd3..ba1995e8c83 100644
--- a/tests/components/wemo/conftest.py
+++ b/tests/components/wemo/conftest.py
@@ -43,13 +43,15 @@ def pywemo_registry_fixture():
@pytest.fixture(name="pywemo_device")
def pywemo_device_fixture(pywemo_registry, pywemo_model):
"""Fixture for WeMoDevice instances."""
- device = create_autospec(getattr(pywemo, pywemo_model), instance=True)
+ cls = getattr(pywemo, pywemo_model)
+ device = create_autospec(cls, instance=True)
device.host = MOCK_HOST
device.port = MOCK_PORT
device.name = MOCK_NAME
device.serialnumber = MOCK_SERIAL_NUMBER
device.model_name = pywemo_model
device.get_state.return_value = 0 # Default to Off
+ device.supports_long_press.return_value = cls.supports_long_press()
url = f"http://{MOCK_HOST}:{MOCK_PORT}/setup.xml"
with patch("pywemo.setup_url_for_address", return_value=url), patch(
diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py
index e584cb5fb39..9289d4a0171 100644
--- a/tests/components/wemo/entity_test_helpers.py
+++ b/tests/components/wemo/entity_test_helpers.py
@@ -13,19 +13,31 @@ from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
)
+from homeassistant.components.wemo.const import SIGNAL_WEMO_STATE_PUSH
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNAVAILABLE
from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.setup import async_setup_component
def _perform_registry_callback(hass, pywemo_registry, pywemo_device):
"""Return a callable method to trigger a state callback from the device."""
- @callback
- def async_callback():
+ async def async_callback():
+ event = asyncio.Event()
+
+ async def event_callback(e, *args):
+ event.set()
+
+ stop_dispatcher_listener = async_dispatcher_connect(
+ hass, SIGNAL_WEMO_STATE_PUSH, event_callback
+ )
# Cause a state update callback to be triggered by the device.
- pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "")
- return hass.async_block_till_done()
+ await hass.async_add_executor_job(
+ pywemo_registry.callbacks[pywemo_device.name], pywemo_device, "", ""
+ )
+ await event.wait()
+ stop_dispatcher_listener()
return async_callback
@@ -63,8 +75,10 @@ async def _async_multiple_call_helper(
"""
# get_state is called outside the event loop. Use non-async Python Event.
event = threading.Event()
+ waiting = asyncio.Event()
def get_update(force_update=True):
+ hass.add_job(waiting.set)
event.wait()
update_polling_method = update_polling_method or pywemo_device.get_state
@@ -77,6 +91,7 @@ async def _async_multiple_call_helper(
)
# Allow the blocked call to return.
+ await waiting.wait()
event.set()
if pending:
await asyncio.wait(pending)
diff --git a/tests/components/wemo/test_device_trigger.py b/tests/components/wemo/test_device_trigger.py
new file mode 100644
index 00000000000..76016469b72
--- /dev/null
+++ b/tests/components/wemo/test_device_trigger.py
@@ -0,0 +1,98 @@
+"""Verify that WeMo device triggers work as expected."""
+import pytest
+from pywemo.subscribe import EVENT_TYPE_LONG_PRESS
+
+from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
+from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
+from homeassistant.components.wemo.const import DOMAIN, WEMO_SUBSCRIPTION_EVENT
+from homeassistant.const import (
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ CONF_ENTITY_ID,
+ CONF_PLATFORM,
+ CONF_TYPE,
+)
+from homeassistant.setup import async_setup_component
+
+from tests.common import (
+ assert_lists_same,
+ async_get_device_automations,
+ async_mock_service,
+)
+
+MOCK_DEVICE_ID = "some-device-id"
+DATA_MESSAGE = {"message": "service-called"}
+
+
+@pytest.fixture
+def pywemo_model():
+ """Pywemo Dimmer models use the light platform (WemoDimmer class)."""
+ return "Dimmer"
+
+
+async def setup_automation(hass, device_id, trigger_type):
+ """Set up an automation trigger for testing triggering."""
+ return await async_setup_component(
+ hass,
+ AUTOMATION_DOMAIN,
+ {
+ AUTOMATION_DOMAIN: [
+ {
+ "trigger": {
+ CONF_PLATFORM: "device",
+ CONF_DOMAIN: DOMAIN,
+ CONF_DEVICE_ID: device_id,
+ CONF_TYPE: trigger_type,
+ },
+ "action": {
+ "service": "test.automation",
+ "data": DATA_MESSAGE,
+ },
+ },
+ ]
+ },
+ )
+
+
+async def test_get_triggers(hass, wemo_entity):
+ """Test that the triggers appear for a supported device."""
+ assert wemo_entity.device_id is not None
+
+ expected_triggers = [
+ {
+ CONF_DEVICE_ID: wemo_entity.device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_PLATFORM: "device",
+ CONF_TYPE: EVENT_TYPE_LONG_PRESS,
+ },
+ {
+ CONF_DEVICE_ID: wemo_entity.device_id,
+ CONF_DOMAIN: LIGHT_DOMAIN,
+ CONF_ENTITY_ID: wemo_entity.entity_id,
+ CONF_PLATFORM: "device",
+ CONF_TYPE: "turned_off",
+ },
+ {
+ CONF_DEVICE_ID: wemo_entity.device_id,
+ CONF_DOMAIN: LIGHT_DOMAIN,
+ CONF_ENTITY_ID: wemo_entity.entity_id,
+ CONF_PLATFORM: "device",
+ CONF_TYPE: "turned_on",
+ },
+ ]
+ triggers = await async_get_device_automations(
+ hass, "trigger", wemo_entity.device_id
+ )
+ assert_lists_same(triggers, expected_triggers)
+
+
+async def test_fires_on_long_press(hass):
+ """Test wemo long press trigger firing."""
+ assert await setup_automation(hass, MOCK_DEVICE_ID, EVENT_TYPE_LONG_PRESS)
+ calls = async_mock_service(hass, "test", "automation")
+
+ message = {CONF_DEVICE_ID: MOCK_DEVICE_ID, CONF_TYPE: EVENT_TYPE_LONG_PRESS}
+ hass.bus.async_fire(WEMO_SUBSCRIPTION_EVENT, message)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data == DATA_MESSAGE
diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py
index c44bdb659c5..f34e9bd0471 100644
--- a/tests/components/wemo/test_init.py
+++ b/tests/components/wemo/test_init.py
@@ -110,6 +110,7 @@ async def test_discovery(hass, pywemo_registry):
device.serialnumber = f"{MOCK_SERIAL_NUMBER}_{counter}"
device.model_name = "Motion"
device.get_state.return_value = 0 # Default to Off
+ device.supports_long_press.return_value = False
return device
pywemo_devices = [create_device(0), create_device(1)]
diff --git a/tests/components/wemo/test_wemo_device.py b/tests/components/wemo/test_wemo_device.py
new file mode 100644
index 00000000000..38727a28424
--- /dev/null
+++ b/tests/components/wemo/test_wemo_device.py
@@ -0,0 +1,40 @@
+"""Tests for wemo_device.py."""
+from unittest.mock import patch
+
+import pytest
+from pywemo import PyWeMoException
+
+from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC, wemo_device
+from homeassistant.components.wemo.const import DOMAIN
+from homeassistant.helpers import device_registry
+from homeassistant.setup import async_setup_component
+
+from .conftest import MOCK_HOST
+
+
+@pytest.fixture
+def pywemo_model():
+ """Pywemo Dimmer models use the light platform (WemoDimmer class)."""
+ return "Dimmer"
+
+
+async def test_async_register_device_longpress_fails(hass, pywemo_device):
+ """Device is still registered if ensure_long_press_virtual_device fails."""
+ with patch.object(pywemo_device, "ensure_long_press_virtual_device") as elp:
+ elp.side_effect = PyWeMoException
+ assert await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: {
+ CONF_DISCOVERY: False,
+ CONF_STATIC: [MOCK_HOST],
+ },
+ },
+ )
+ await hass.async_block_till_done()
+ dr = device_registry.async_get(hass)
+ device_entries = list(dr.devices.values())
+ assert len(device_entries) == 1
+ device_wrapper = wemo_device.async_get_device(hass, device_entries[0].id)
+ assert device_wrapper.supports_long_press is False
diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py
index d16b4d083e8..dd7d83876f8 100644
--- a/tests/components/wilight/__init__.py
+++ b/tests/components/wilight/__init__.py
@@ -41,7 +41,7 @@ MOCK_SSDP_DISCOVERY_INFO_P_B = {
ATTR_UPNP_SERIAL: UPNP_SERIAL,
}
-MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER = {
+MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER = {
ATTR_SSDP_LOCATION: SSDP_LOCATION,
ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER_NOT_WILIGHT,
ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B,
@@ -49,7 +49,7 @@ MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER = {
ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL,
}
-MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER = {
+MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER = {
ATTR_SSDP_LOCATION: SSDP_LOCATION,
ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B,
ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER,
diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py
index 42f6aa592b0..4835167715d 100644
--- a/tests/components/wilight/test_config_flow.py
+++ b/tests/components/wilight/test_config_flow.py
@@ -21,9 +21,9 @@ from tests.common import MockConfigEntry
from tests.components.wilight import (
CONF_COMPONENTS,
HOST,
- MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER,
+ MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER,
MOCK_SSDP_DISCOVERY_INFO_P_B,
- MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER,
+ MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER,
UPNP_MODEL_NAME_P_B,
UPNP_SERIAL,
WILIGHT_ID,
@@ -71,7 +71,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None:
async def test_ssdp_not_wilight_abort_1(hass: HomeAssistant) -> None:
"""Test that the ssdp aborts not_wilight."""
- discovery_info = MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER.copy()
+ discovery_info = MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER.copy()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info
)
@@ -83,7 +83,7 @@ async def test_ssdp_not_wilight_abort_1(hass: HomeAssistant) -> None:
async def test_ssdp_not_wilight_abort_2(hass: HomeAssistant) -> None:
"""Test that the ssdp aborts not_wilight."""
- discovery_info = MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER.copy()
+ discovery_info = MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER.copy()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info
)
diff --git a/tests/components/wled/__init__.py b/tests/components/wled/__init__.py
index a39d1ef6453..40723f54294 100644
--- a/tests/components/wled/__init__.py
+++ b/tests/components/wled/__init__.py
@@ -1,58 +1 @@
"""Tests for the WLED integration."""
-
-import json
-
-from homeassistant.components.wled.const import DOMAIN
-from homeassistant.const import CONF_HOST, CONF_MAC, CONTENT_TYPE_JSON
-from homeassistant.core import HomeAssistant
-
-from tests.common import MockConfigEntry, load_fixture
-from tests.test_util.aiohttp import AiohttpClientMocker
-
-
-async def init_integration(
- hass: HomeAssistant,
- aioclient_mock: AiohttpClientMocker,
- rgbw: bool = False,
- skip_setup: bool = False,
-) -> MockConfigEntry:
- """Set up the WLED integration in Home Assistant."""
-
- fixture = "wled/rgb.json" if not rgbw else "wled/rgbw.json"
- data = json.loads(load_fixture(fixture))
-
- aioclient_mock.get(
- "http://192.168.1.123:80/json/",
- json=data,
- headers={"Content-Type": CONTENT_TYPE_JSON},
- )
-
- aioclient_mock.post(
- "http://192.168.1.123:80/json/state",
- json=data["state"],
- headers={"Content-Type": CONTENT_TYPE_JSON},
- )
-
- aioclient_mock.get(
- "http://192.168.1.123:80/json/info",
- json=data["info"],
- headers={"Content-Type": CONTENT_TYPE_JSON},
- )
-
- aioclient_mock.get(
- "http://192.168.1.123:80/json/state",
- json=data["state"],
- headers={"Content-Type": CONTENT_TYPE_JSON},
- )
-
- entry = MockConfigEntry(
- domain=DOMAIN, data={CONF_HOST: "192.168.1.123", CONF_MAC: "aabbccddeeff"}
- )
-
- entry.add_to_hass(hass)
-
- if not skip_setup:
- await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
- return entry
diff --git a/tests/components/wled/conftest.py b/tests/components/wled/conftest.py
index 7b8eb9cd50c..80b351a20f1 100644
--- a/tests/components/wled/conftest.py
+++ b/tests/components/wled/conftest.py
@@ -1,2 +1,80 @@
-"""wled conftest."""
+"""Fixtures for WLED integration tests."""
+import json
+from typing import Generator
+from unittest.mock import MagicMock, patch
+
+import pytest
+from wled import Device as WLEDDevice
+
+from homeassistant.components.wled.const import DOMAIN
+from homeassistant.const import CONF_HOST, CONF_MAC
+from homeassistant.core import HomeAssistant
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry, load_fixture
from tests.components.light.conftest import mock_light_profiles # noqa: F401
+
+
+@pytest.fixture(autouse=True)
+async def mock_persistent_notification(hass: HomeAssistant) -> None:
+ """Set up component for persistent notifications."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+
+@pytest.fixture
+def mock_config_entry() -> MockConfigEntry:
+ """Return the default mocked config entry."""
+ return MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_HOST: "192.168.1.123", CONF_MAC: "aabbccddeeff"},
+ )
+
+
+@pytest.fixture
+def mock_setup_entry() -> Generator[None, None, None]:
+ """Mock setting up a config entry."""
+ with patch("homeassistant.components.wled.async_setup_entry", return_value=True):
+ yield
+
+
+@pytest.fixture
+def mock_wled_config_flow(
+ request: pytest.FixtureRequest,
+) -> Generator[None, MagicMock, None]:
+ """Return a mocked WLED client."""
+ with patch(
+ "homeassistant.components.wled.config_flow.WLED", autospec=True
+ ) as wled_mock:
+ wled = wled_mock.return_value
+ wled.update.return_value = WLEDDevice(json.loads(load_fixture("wled/rgb.json")))
+ yield wled
+
+
+@pytest.fixture
+def mock_wled(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
+ """Return a mocked WLED client."""
+ fixture: str = "wled/rgb.json"
+ if hasattr(request, "param") and request.param:
+ fixture = request.param
+
+ device = WLEDDevice(json.loads(load_fixture(fixture)))
+ with patch(
+ "homeassistant.components.wled.coordinator.WLED", autospec=True
+ ) as wled_mock:
+ wled = wled_mock.return_value
+ wled.update.return_value = device
+ wled.connected = False
+ yield wled
+
+
+@pytest.fixture
+async def init_integration(
+ hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_wled: MagicMock
+) -> MockConfigEntry:
+ """Set up the WLED integration for testing."""
+ mock_config_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ return mock_config_entry
diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py
index e828c632451..842e7e332e0 100644
--- a/tests/components/wled/test_config_flow.py
+++ b/tests/components/wled/test_config_flow.py
@@ -1,12 +1,11 @@
"""Tests for the WLED config flow."""
-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock
-import aiohttp
from wled import WLEDConnectionError
-from homeassistant.components.wled.const import DOMAIN
+from homeassistant.components.wled.const import CONF_KEEP_MASTER_LIGHT, DOMAIN
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
-from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON
+from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
@@ -14,22 +13,13 @@ from homeassistant.data_entry_flow import (
RESULT_TYPE_FORM,
)
-from . import init_integration
-
-from tests.common import load_fixture
-from tests.test_util.aiohttp import AiohttpClientMocker
+from tests.common import MockConfigEntry
async def test_full_user_flow_implementation(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant, mock_wled_config_flow: MagicMock, mock_setup_entry: None
) -> None:
"""Test the full manual user flow from start to finish."""
- aioclient_mock.get(
- "http://192.168.1.123:80/json/",
- text=load_fixture("wled/rgb.json"),
- headers={"Content-Type": CONTENT_TYPE_JSON},
- )
-
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
@@ -51,15 +41,9 @@ async def test_full_user_flow_implementation(
async def test_full_zeroconf_flow_implementation(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant, mock_wled_config_flow: MagicMock, mock_setup_entry: None
) -> None:
"""Test the full manual user flow from start to finish."""
- aioclient_mock.get(
- "http://192.168.1.123:80/json/",
- text=load_fixture("wled/rgb.json"),
- headers={"Content-Type": CONTENT_TYPE_JSON},
- )
-
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
@@ -91,13 +75,11 @@ async def test_full_zeroconf_flow_implementation(
assert result2["data"][CONF_MAC] == "aabbccddeeff"
-@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError)
async def test_connection_error(
- update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant, mock_wled_config_flow: MagicMock
) -> None:
"""Test we show user form on WLED connection error."""
- aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError)
-
+ mock_wled_config_flow.update.side_effect = WLEDConnectionError
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
@@ -109,12 +91,11 @@ async def test_connection_error(
assert result.get("errors") == {"base": "cannot_connect"}
-@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError)
async def test_zeroconf_connection_error(
- update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant, mock_wled_config_flow: MagicMock
) -> None:
"""Test we abort zeroconf flow on WLED connection error."""
- aioclient_mock.get("http://192.168.1.123/json/", exc=aiohttp.ClientError)
+ mock_wled_config_flow.update.side_effect = WLEDConnectionError
result = await hass.config_entries.flow.async_init(
DOMAIN,
@@ -126,12 +107,11 @@ async def test_zeroconf_connection_error(
assert result.get("reason") == "cannot_connect"
-@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError)
async def test_zeroconf_confirm_connection_error(
- update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant, mock_wled_config_flow: MagicMock
) -> None:
"""Test we abort zeroconf flow on WLED connection error."""
- aioclient_mock.get("http://192.168.1.123:80/json/", exc=aiohttp.ClientError)
+ mock_wled_config_flow.update.side_effect = WLEDConnectionError
result = await hass.config_entries.flow.async_init(
DOMAIN,
@@ -148,11 +128,11 @@ async def test_zeroconf_confirm_connection_error(
async def test_user_device_exists_abort(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant,
+ init_integration: MagicMock,
+ mock_wled_config_flow: MagicMock,
) -> None:
"""Test we abort zeroconf flow if WLED device already configured."""
- await init_integration(hass, aioclient_mock)
-
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
@@ -164,11 +144,11 @@ async def test_user_device_exists_abort(
async def test_zeroconf_device_exists_abort(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant,
+ init_integration: MagicMock,
+ mock_wled_config_flow: MagicMock,
) -> None:
"""Test we abort zeroconf flow if WLED device already configured."""
- await init_integration(hass, aioclient_mock)
-
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
@@ -180,11 +160,11 @@ async def test_zeroconf_device_exists_abort(
async def test_zeroconf_with_mac_device_exists_abort(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled_config_flow: MagicMock,
) -> None:
"""Test we abort zeroconf flow if WLED device already configured."""
- await init_integration(hass, aioclient_mock)
-
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
@@ -197,3 +177,26 @@ async def test_zeroconf_with_mac_device_exists_abort(
assert result.get("type") == RESULT_TYPE_ABORT
assert result.get("reason") == "already_configured"
+
+
+async def test_options_flow(
+ hass: HomeAssistant, mock_config_entry: MockConfigEntry
+) -> None:
+ """Test options config flow."""
+ mock_config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
+
+ assert result.get("type") == RESULT_TYPE_FORM
+ assert result.get("step_id") == "init"
+ assert "flow_id" in result
+
+ result2 = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_KEEP_MASTER_LIGHT: True},
+ )
+
+ assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
+ assert result2.get("data") == {
+ CONF_KEEP_MASTER_LIGHT: True,
+ }
diff --git a/tests/components/wled/test_coordinator.py b/tests/components/wled/test_coordinator.py
new file mode 100644
index 00000000000..47190604238
--- /dev/null
+++ b/tests/components/wled/test_coordinator.py
@@ -0,0 +1,196 @@
+"""Tests for the coordinator of the WLED integration."""
+import asyncio
+from copy import deepcopy
+from typing import Callable
+from unittest.mock import MagicMock
+
+import pytest
+from wled import (
+ Device as WLEDDevice,
+ WLEDConnectionClosed,
+ WLEDConnectionError,
+ WLEDError,
+)
+
+from homeassistant.components.wled.const import SCAN_INTERVAL
+from homeassistant.const import (
+ EVENT_HOMEASSISTANT_STOP,
+ STATE_OFF,
+ STATE_ON,
+ STATE_UNAVAILABLE,
+)
+from homeassistant.core import HomeAssistant
+import homeassistant.util.dt as dt_util
+
+from tests.common import MockConfigEntry, async_fire_time_changed
+
+
+async def test_not_supporting_websocket(
+ hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock
+) -> None:
+ """Ensure no WebSocket attempt is made if non-WebSocket device."""
+ assert mock_wled.connect.call_count == 0
+
+
+@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True)
+async def test_websocket_already_connected(
+ hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock
+) -> None:
+ """Ensure no a second WebSocket connection is made, if already connected."""
+ assert mock_wled.connect.call_count == 1
+
+ mock_wled.connected = True
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ assert mock_wled.connect.call_count == 1
+
+
+@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True)
+async def test_websocket_connect_error_no_listen(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+) -> None:
+ """Ensure we don't start listening if WebSocket connection failed."""
+ assert mock_wled.connect.call_count == 1
+ assert mock_wled.listen.call_count == 1
+
+ mock_wled.connect.side_effect = WLEDConnectionError
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ assert mock_wled.connect.call_count == 2
+ assert mock_wled.listen.call_count == 1
+
+
+@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True)
+async def test_websocket(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+) -> None:
+ """Test WebSocket connection."""
+ state = hass.states.get("light.wled_websocket")
+ assert state
+ assert state.state == STATE_ON
+
+ # There is no Future in place yet...
+ assert mock_wled.connect.call_count == 1
+ assert mock_wled.listen.call_count == 1
+ assert mock_wled.disconnect.call_count == 1
+
+ connection_connected = asyncio.Future()
+ connection_finished = asyncio.Future()
+
+ async def connect(callback: Callable[[WLEDDevice], None]):
+ connection_connected.set_result(callback)
+ await connection_finished
+
+ # Mock out wled.listen with a Future
+ mock_wled.listen.side_effect = connect
+
+ # Mock out the event bus
+ mock_bus = MagicMock()
+ hass.bus = mock_bus
+
+ # Next refresh it should connect
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ callback = await connection_connected
+
+ # Connected to WebSocket, disconnect not called
+ # listening for Home Assistant to stop
+ assert mock_wled.connect.call_count == 2
+ assert mock_wled.listen.call_count == 2
+ assert mock_wled.disconnect.call_count == 1
+ assert mock_bus.async_listen_once.call_count == 1
+ assert (
+ mock_bus.async_listen_once.call_args_list[0][0][0] == EVENT_HOMEASSISTANT_STOP
+ )
+ assert (
+ mock_bus.async_listen_once.call_args_list[0][0][1].__name__ == "close_websocket"
+ )
+ assert mock_bus.async_listen_once.return_value.call_count == 0
+
+ # Send update from WebSocket
+ updated_device = deepcopy(mock_wled.update.return_value)
+ updated_device.state.on = False
+ callback(updated_device)
+ await hass.async_block_till_done()
+
+ # Check if entity updated
+ state = hass.states.get("light.wled_websocket")
+ assert state
+ assert state.state == STATE_OFF
+
+ # Resolve Future with a connection losed.
+ connection_finished.set_exception(WLEDConnectionClosed)
+ await hass.async_block_till_done()
+
+ # Disconnect called, unsubbed Home Assistant stop listener
+ assert mock_wled.disconnect.call_count == 2
+ assert mock_bus.async_listen_once.return_value.call_count == 1
+
+ # Light still available, as polling takes over
+ state = hass.states.get("light.wled_websocket")
+ assert state
+ assert state.state == STATE_OFF
+
+
+@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True)
+async def test_websocket_error(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+) -> None:
+ """Test WebSocket connection erroring out, marking lights unavailable."""
+ state = hass.states.get("light.wled_websocket")
+ assert state
+ assert state.state == STATE_ON
+
+ connection_connected = asyncio.Future()
+ connection_finished = asyncio.Future()
+
+ async def connect(callback: Callable[[WLEDDevice], None]):
+ connection_connected.set_result(None)
+ await connection_finished
+
+ mock_wled.listen.side_effect = connect
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await connection_connected
+
+ # Resolve Future with an error.
+ connection_finished.set_exception(WLEDError)
+ await hass.async_block_till_done()
+
+ # Light no longer available as an error occurred
+ state = hass.states.get("light.wled_websocket")
+ assert state
+ assert state.state == STATE_UNAVAILABLE
+
+
+@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True)
+async def test_websocket_disconnect_on_home_assistant_stop(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+) -> None:
+ """Ensure WebSocket is disconnected when Home Assistant stops."""
+ assert mock_wled.disconnect.call_count == 1
+ connection_connected = asyncio.Future()
+ connection_finished = asyncio.Future()
+
+ async def connect(callback: Callable[[WLEDDevice], None]):
+ connection_connected.set_result(None)
+ await connection_finished
+
+ mock_wled.listen.side_effect = connect
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await connection_connected
+
+ assert mock_wled.disconnect.call_count == 1
+
+ hass.bus.fire(EVENT_HOMEASSISTANT_STOP)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+ assert mock_wled.disconnect.call_count == 2
diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py
index 8db7e266e80..01821262389 100644
--- a/tests/components/wled/test_init.py
+++ b/tests/components/wled/test_init.py
@@ -1,40 +1,70 @@
"""Tests for the WLED integration."""
-from unittest.mock import MagicMock, patch
+import asyncio
+from typing import Callable
+from unittest.mock import AsyncMock, MagicMock, patch
+import pytest
from wled import WLEDConnectionError
from homeassistant.components.wled.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
-from tests.components.wled import init_integration
-from tests.test_util.aiohttp import AiohttpClientMocker
+from tests.common import MockConfigEntry
-@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError)
-async def test_config_entry_not_ready(
- mock_update: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
-) -> None:
- """Test the WLED configuration entry not ready."""
- entry = await init_integration(hass, aioclient_mock)
- assert entry.state is ConfigEntryState.SETUP_RETRY
-
-
-async def test_unload_config_entry(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True)
+async def test_load_unload_config_entry(
+ hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_wled: AsyncMock
) -> None:
"""Test the WLED configuration entry unloading."""
- entry = await init_integration(hass, aioclient_mock)
- assert hass.data[DOMAIN]
+ connection_connected = asyncio.Future()
+ connection_finished = asyncio.Future()
- await hass.config_entries.async_unload(entry.entry_id)
+ async def connect(callback: Callable):
+ connection_connected.set_result(None)
+ await connection_finished
+
+ # Mock out wled.listen with a Future
+ mock_wled.listen.side_effect = connect
+
+ mock_config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
+ await connection_connected
+
+ # Ensure config entry is loaded and are connected
+ assert mock_config_entry.state is ConfigEntryState.LOADED
+ assert mock_wled.connect.call_count == 1
+ assert mock_wled.disconnect.call_count == 0
+
+ await hass.config_entries.async_unload(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ # Ensure everything is cleaned up nicely and are disconnected
+ assert mock_wled.disconnect.call_count == 1
assert not hass.data.get(DOMAIN)
-async def test_setting_unique_id(hass, aioclient_mock):
- """Test we set unique ID if not set yet."""
- entry = await init_integration(hass, aioclient_mock)
+@patch(
+ "homeassistant.components.wled.coordinator.WLED.request",
+ side_effect=WLEDConnectionError,
+)
+async def test_config_entry_not_ready(
+ mock_request: MagicMock, hass: HomeAssistant, mock_config_entry: MockConfigEntry
+) -> None:
+ """Test the WLED configuration entry not ready."""
+ mock_config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert mock_request.call_count == 1
+ assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
+
+
+async def test_setting_unique_id(
+ hass: HomeAssistant, init_integration: MockConfigEntry
+) -> None:
+ """Test we set unique ID if not set yet."""
assert hass.data[DOMAIN]
- assert entry.unique_id == "aabbccddeeff"
+ assert init_integration.unique_id == "aabbccddeeff"
diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py
index 0077cea0202..d61b675e2f2 100644
--- a/tests/components/wled/test_light.py
+++ b/tests/components/wled/test_light.py
@@ -1,20 +1,19 @@
"""Tests for the WLED light platform."""
import json
-from unittest.mock import patch
+from unittest.mock import MagicMock
-from wled import Device as WLEDDevice, WLEDConnectionError
+import pytest
+from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
- ATTR_COLOR_TEMP,
ATTR_EFFECT,
ATTR_HS_COLOR,
ATTR_RGB_COLOR,
+ ATTR_RGBW_COLOR,
ATTR_TRANSITION,
- ATTR_WHITE_VALUE,
DOMAIN as LIGHT_DOMAIN,
)
-from homeassistant.components.wled import SCAN_INTERVAL
from homeassistant.components.wled.const import (
ATTR_INTENSITY,
ATTR_PALETTE,
@@ -22,7 +21,9 @@ from homeassistant.components.wled.const import (
ATTR_PRESET,
ATTR_REVERSE,
ATTR_SPEED,
+ CONF_KEEP_MASTER_LIGHT,
DOMAIN,
+ SCAN_INTERVAL,
SERVICE_EFFECT,
SERVICE_PRESET,
)
@@ -39,21 +40,17 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.util.dt as dt_util
-from tests.common import async_fire_time_changed, load_fixture
-from tests.components.wled import init_integration
-from tests.test_util.aiohttp import AiohttpClientMocker
+from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture
async def test_rgb_light_state(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant, init_integration: MockConfigEntry
) -> None:
"""Test the creation and values of the WLED lights."""
- await init_integration(hass, aioclient_mock)
-
entity_registry = er.async_get(hass)
# First segment of the strip
- state = hass.states.get("light.wled_rgb_light_segment_0")
+ state = hass.states.get("light.wled_rgb_light")
assert state
assert state.attributes.get(ATTR_BRIGHTNESS) == 127
assert state.attributes.get(ATTR_EFFECT) == "Solid"
@@ -67,7 +64,7 @@ async def test_rgb_light_state(
assert state.attributes.get(ATTR_SPEED) == 32
assert state.state == STATE_ON
- entry = entity_registry.async_get("light.wled_rgb_light_segment_0")
+ entry = entity_registry.async_get("light.wled_rgb_light")
assert entry
assert entry.unique_id == "aabbccddeeff_0"
@@ -102,533 +99,542 @@ async def test_rgb_light_state(
async def test_segment_change_state(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
) -> None:
"""Test the change of state of the WLED segments."""
- await init_integration(hass, aioclient_mock)
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_TRANSITION: 5},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.segment.call_count == 1
+ mock_wled.segment.assert_called_with(
+ on=False,
+ segment_id=0,
+ transition=50,
+ )
- with patch("wled.WLED.segment") as light_mock:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_TRANSITION: 5},
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- on=False,
- segment_id=0,
- transition=50,
- )
-
- with patch("wled.WLED.segment") as light_mock:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_BRIGHTNESS: 42,
- ATTR_EFFECT: "Chase",
- ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
- ATTR_RGB_COLOR: [255, 0, 0],
- ATTR_TRANSITION: 5,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- brightness=42,
- color_primary=(255, 0, 0),
- effect="Chase",
- on=True,
- segment_id=0,
- transition=50,
- )
-
- with patch("wled.WLED.segment") as light_mock:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_COLOR_TEMP: 400},
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- color_primary=(255, 159, 70),
- on=True,
- segment_id=0,
- )
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_BRIGHTNESS: 42,
+ ATTR_EFFECT: "Chase",
+ ATTR_ENTITY_ID: "light.wled_rgb_light",
+ ATTR_RGB_COLOR: [255, 0, 0],
+ ATTR_TRANSITION: 5,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.segment.call_count == 2
+ mock_wled.segment.assert_called_with(
+ brightness=42,
+ color_primary=(255, 0, 0),
+ effect="Chase",
+ on=True,
+ segment_id=0,
+ transition=50,
+ )
async def test_master_change_state(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
) -> None:
"""Test the change of state of the WLED master light control."""
- await init_integration(hass, aioclient_mock)
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.wled_rgb_light_master", ATTR_TRANSITION: 5},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.master.call_count == 1
+ mock_wled.master.assert_called_with(
+ on=False,
+ transition=50,
+ )
- with patch("wled.WLED.master") as light_mock:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "light.wled_rgb_light_master", ATTR_TRANSITION: 5},
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- on=False,
- transition=50,
- )
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_BRIGHTNESS: 42,
+ ATTR_ENTITY_ID: "light.wled_rgb_light_master",
+ ATTR_TRANSITION: 5,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.master.call_count == 2
+ mock_wled.master.assert_called_with(
+ brightness=42,
+ on=True,
+ transition=50,
+ )
- with patch("wled.WLED.master") as light_mock:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_BRIGHTNESS: 42,
- ATTR_ENTITY_ID: "light.wled_rgb_light_master",
- ATTR_TRANSITION: 5,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- brightness=42,
- on=True,
- transition=50,
- )
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.wled_rgb_light_master", ATTR_TRANSITION: 5},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.master.call_count == 3
+ mock_wled.master.assert_called_with(
+ on=False,
+ transition=50,
+ )
- with patch("wled.WLED.master") as light_mock:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "light.wled_rgb_light_master", ATTR_TRANSITION: 5},
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- on=False,
- transition=50,
- )
-
- with patch("wled.WLED.master") as light_mock:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_BRIGHTNESS: 42,
- ATTR_ENTITY_ID: "light.wled_rgb_light_master",
- ATTR_TRANSITION: 5,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- brightness=42,
- on=True,
- transition=50,
- )
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_BRIGHTNESS: 42,
+ ATTR_ENTITY_ID: "light.wled_rgb_light_master",
+ ATTR_TRANSITION: 5,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.master.call_count == 4
+ mock_wled.master.assert_called_with(
+ brightness=42,
+ on=True,
+ transition=50,
+ )
+@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True)
async def test_dynamically_handle_segments(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
) -> None:
"""Test if a new/deleted segment is dynamically added/removed."""
- await init_integration(hass, aioclient_mock)
+ master = hass.states.get("light.wled_rgb_light_master")
+ segment0 = hass.states.get("light.wled_rgb_light")
+ segment1 = hass.states.get("light.wled_rgb_light_segment_1")
+ assert segment0
+ assert segment0.state == STATE_ON
+ assert not master
+ assert not segment1
- assert hass.states.get("light.wled_rgb_light_master")
- assert hass.states.get("light.wled_rgb_light_segment_0")
- assert hass.states.get("light.wled_rgb_light_segment_1")
+ return_value = mock_wled.update.return_value
+ mock_wled.update.return_value = WLEDDevice(
+ json.loads(load_fixture("wled/rgb.json"))
+ )
- data = json.loads(load_fixture("wled/rgb_single_segment.json"))
- device = WLEDDevice(data)
-
- # Test removal if segment went missing, including the master entity
- with patch(
- "homeassistant.components.wled.WLED.update",
- return_value=device,
- ):
- async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
- await hass.async_block_till_done()
- assert hass.states.get("light.wled_rgb_light_segment_0")
- assert not hass.states.get("light.wled_rgb_light_segment_1")
- assert not hass.states.get("light.wled_rgb_light_master")
-
- # Test adding if segment shows up again, including the master entity
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
- assert hass.states.get("light.wled_rgb_light_master")
- assert hass.states.get("light.wled_rgb_light_segment_0")
- assert hass.states.get("light.wled_rgb_light_segment_1")
+ master = hass.states.get("light.wled_rgb_light_master")
+ segment0 = hass.states.get("light.wled_rgb_light")
+ segment1 = hass.states.get("light.wled_rgb_light_segment_1")
+ assert master
+ assert master.state == STATE_ON
+ assert segment0
+ assert segment0.state == STATE_ON
+ assert segment1
+ assert segment1.state == STATE_ON
+
+ # Test adding if segment shows up again, including the master entity
+ mock_wled.update.return_value = return_value
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ master = hass.states.get("light.wled_rgb_light_master")
+ segment0 = hass.states.get("light.wled_rgb_light")
+ segment1 = hass.states.get("light.wled_rgb_light_segment_1")
+ assert master
+ assert master.state == STATE_UNAVAILABLE
+ assert segment0
+ assert segment0.state == STATE_ON
+ assert segment1
+ assert segment1.state == STATE_UNAVAILABLE
+@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True)
async def test_single_segment_behavior(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
) -> None:
"""Test the behavior of the integration with a single segment."""
- await init_integration(hass, aioclient_mock)
+ device = mock_wled.update.return_value
- data = json.loads(load_fixture("wled/rgb_single_segment.json"))
- device = WLEDDevice(data)
-
- # Test absent master
- with patch(
- "homeassistant.components.wled.WLED.update",
- return_value=device,
- ):
- async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
- await hass.async_block_till_done()
-
- assert not hass.states.get("light.wled_rgb_light_master")
-
- state = hass.states.get("light.wled_rgb_light_segment_0")
- assert state
- assert state.state == STATE_ON
+ assert not hass.states.get("light.wled_rgb_light_master")
+ state = hass.states.get("light.wled_rgb_light")
+ assert state
+ assert state.state == STATE_ON
# Test segment brightness takes master into account
device.state.brightness = 100
device.state.segments[0].brightness = 255
- with patch(
- "homeassistant.components.wled.WLED.update",
- return_value=device,
- ):
- async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
- await hass.async_block_till_done()
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
- state = hass.states.get("light.wled_rgb_light_segment_0")
- assert state
- assert state.attributes.get(ATTR_BRIGHTNESS) == 100
+ state = hass.states.get("light.wled_rgb_light")
+ assert state
+ assert state.attributes.get(ATTR_BRIGHTNESS) == 100
# Test segment is off when master is off
device.state.on = False
- with patch(
- "homeassistant.components.wled.WLED.update",
- return_value=device,
- ):
- async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
- await hass.async_block_till_done()
- state = hass.states.get("light.wled_rgb_light_segment_0")
- assert state
- assert state.state == STATE_OFF
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+ state = hass.states.get("light.wled_rgb_light")
+ assert state
+ assert state.state == STATE_OFF
# Test master is turned off when turning off a single segment
- with patch("wled.WLED.master") as master_mock:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_TRANSITION: 5},
- blocking=True,
- )
- await hass.async_block_till_done()
- master_mock.assert_called_once_with(
- on=False,
- transition=50,
- )
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_TRANSITION: 5},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.master.call_count == 1
+ mock_wled.master.assert_called_with(
+ on=False,
+ transition=50,
+ )
# Test master is turned on when turning on a single segment, and segment
# brightness is set to 255.
- with patch("wled.WLED.master") as master_mock, patch(
- "wled.WLED.segment"
- ) as segment_mock:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
- ATTR_TRANSITION: 5,
- ATTR_BRIGHTNESS: 42,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- master_mock.assert_called_once_with(on=True, transition=50, brightness=42)
- segment_mock.assert_called_once_with(on=True, segment_id=0, brightness=255)
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: "light.wled_rgb_light",
+ ATTR_TRANSITION: 5,
+ ATTR_BRIGHTNESS: 42,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.segment.call_count == 1
+ assert mock_wled.master.call_count == 2
+ mock_wled.segment.assert_called_with(on=True, segment_id=0, brightness=255)
+ mock_wled.master.assert_called_with(on=True, transition=50, brightness=42)
async def test_light_error(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+ caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the WLED lights."""
- aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400)
- await init_integration(hass, aioclient_mock)
+ mock_wled.segment.side_effect = WLEDError
- with patch("homeassistant.components.wled.WLED.update"):
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0"},
- blocking=True,
- )
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.wled_rgb_light"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
- state = hass.states.get("light.wled_rgb_light_segment_0")
- assert state.state == STATE_ON
- assert "Invalid response from API" in caplog.text
+ state = hass.states.get("light.wled_rgb_light")
+ assert state
+ assert state.state == STATE_ON
+ assert "Invalid response from API" in caplog.text
+ assert mock_wled.segment.call_count == 1
+ mock_wled.segment.assert_called_with(on=False, segment_id=0, transition=None)
async def test_light_connection_error(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+ caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the WLED switches."""
- await init_integration(hass, aioclient_mock)
+ mock_wled.segment.side_effect = WLEDConnectionError
- with patch("homeassistant.components.wled.WLED.update"), patch(
- "homeassistant.components.wled.WLED.segment", side_effect=WLEDConnectionError
- ):
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0"},
- blocking=True,
- )
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.wled_rgb_light"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
- state = hass.states.get("light.wled_rgb_light_segment_0")
- assert state.state == STATE_UNAVAILABLE
+ state = hass.states.get("light.wled_rgb_light")
+ assert state
+ assert state.state == STATE_UNAVAILABLE
+ assert "Error communicating with API" in caplog.text
+ assert mock_wled.segment.call_count == 1
+ mock_wled.segment.assert_called_with(on=False, segment_id=0, transition=None)
+@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
async def test_rgbw_light(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock
) -> None:
"""Test RGBW support for WLED."""
- await init_integration(hass, aioclient_mock, rgbw=True)
-
state = hass.states.get("light.wled_rgbw_light")
+ assert state
assert state.state == STATE_ON
- assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0)
- assert state.attributes.get(ATTR_WHITE_VALUE) == 139
+ assert state.attributes.get(ATTR_RGBW_COLOR) == (255, 0, 0, 139)
- with patch("wled.WLED.segment") as light_mock:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_COLOR_TEMP: 400},
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- on=True,
- segment_id=0,
- color_primary=(255, 159, 70, 139),
- )
-
- with patch("wled.WLED.segment") as light_mock:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_WHITE_VALUE: 100},
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- color_primary=(255, 0, 0, 100),
- on=True,
- segment_id=0,
- )
-
- with patch("wled.WLED.segment") as light_mock:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_ENTITY_ID: "light.wled_rgbw_light",
- ATTR_RGB_COLOR: (255, 255, 255),
- ATTR_WHITE_VALUE: 100,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- color_primary=(0, 0, 0, 100),
- on=True,
- segment_id=0,
- )
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: "light.wled_rgbw_light",
+ ATTR_RGBW_COLOR: (255, 255, 255, 255),
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.segment.call_count == 1
+ mock_wled.segment.assert_called_with(
+ color_primary=(255, 255, 255, 255),
+ on=True,
+ segment_id=0,
+ )
async def test_effect_service(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock
) -> None:
"""Test the effect service of a WLED light."""
- await init_integration(hass, aioclient_mock)
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_EFFECT,
+ {
+ ATTR_EFFECT: "Rainbow",
+ ATTR_ENTITY_ID: "light.wled_rgb_light",
+ ATTR_INTENSITY: 200,
+ ATTR_PALETTE: "Tiamat",
+ ATTR_REVERSE: True,
+ ATTR_SPEED: 100,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.segment.call_count == 1
+ mock_wled.segment.assert_called_with(
+ effect="Rainbow",
+ intensity=200,
+ palette="Tiamat",
+ reverse=True,
+ segment_id=0,
+ speed=100,
+ )
- with patch("wled.WLED.segment") as light_mock:
- await hass.services.async_call(
- DOMAIN,
- SERVICE_EFFECT,
- {
- ATTR_EFFECT: "Rainbow",
- ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
- ATTR_INTENSITY: 200,
- ATTR_PALETTE: "Tiamat",
- ATTR_REVERSE: True,
- ATTR_SPEED: 100,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- effect="Rainbow",
- intensity=200,
- palette="Tiamat",
- reverse=True,
- segment_id=0,
- speed=100,
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_EFFECT,
+ {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_EFFECT: 9},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.segment.call_count == 2
+ mock_wled.segment.assert_called_with(
+ segment_id=0,
+ effect=9,
+ intensity=None,
+ palette=None,
+ reverse=None,
+ speed=None,
+ )
- with patch("wled.WLED.segment") as light_mock:
- await hass.services.async_call(
- DOMAIN,
- SERVICE_EFFECT,
- {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_EFFECT: 9},
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- segment_id=0,
- effect=9,
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_EFFECT,
+ {
+ ATTR_ENTITY_ID: "light.wled_rgb_light",
+ ATTR_INTENSITY: 200,
+ ATTR_REVERSE: True,
+ ATTR_SPEED: 100,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.segment.call_count == 3
+ mock_wled.segment.assert_called_with(
+ intensity=200,
+ reverse=True,
+ segment_id=0,
+ speed=100,
+ effect=None,
+ palette=None,
+ )
- with patch("wled.WLED.segment") as light_mock:
- await hass.services.async_call(
- DOMAIN,
- SERVICE_EFFECT,
- {
- ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
- ATTR_INTENSITY: 200,
- ATTR_REVERSE: True,
- ATTR_SPEED: 100,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- intensity=200,
- reverse=True,
- segment_id=0,
- speed=100,
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_EFFECT,
+ {
+ ATTR_EFFECT: "Rainbow",
+ ATTR_ENTITY_ID: "light.wled_rgb_light",
+ ATTR_PALETTE: "Tiamat",
+ ATTR_REVERSE: True,
+ ATTR_SPEED: 100,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.segment.call_count == 4
+ mock_wled.segment.assert_called_with(
+ effect="Rainbow",
+ palette="Tiamat",
+ reverse=True,
+ segment_id=0,
+ speed=100,
+ intensity=None,
+ )
- with patch("wled.WLED.segment") as light_mock:
- await hass.services.async_call(
- DOMAIN,
- SERVICE_EFFECT,
- {
- ATTR_EFFECT: "Rainbow",
- ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
- ATTR_PALETTE: "Tiamat",
- ATTR_REVERSE: True,
- ATTR_SPEED: 100,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- effect="Rainbow",
- palette="Tiamat",
- reverse=True,
- segment_id=0,
- speed=100,
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_EFFECT,
+ {
+ ATTR_EFFECT: "Rainbow",
+ ATTR_ENTITY_ID: "light.wled_rgb_light",
+ ATTR_INTENSITY: 200,
+ ATTR_SPEED: 100,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.segment.call_count == 5
+ mock_wled.segment.assert_called_with(
+ effect="Rainbow",
+ intensity=200,
+ segment_id=0,
+ speed=100,
+ palette=None,
+ reverse=None,
+ )
- with patch("wled.WLED.segment") as light_mock:
- await hass.services.async_call(
- DOMAIN,
- SERVICE_EFFECT,
- {
- ATTR_EFFECT: "Rainbow",
- ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
- ATTR_INTENSITY: 200,
- ATTR_SPEED: 100,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- effect="Rainbow",
- intensity=200,
- segment_id=0,
- speed=100,
- )
-
- with patch("wled.WLED.segment") as light_mock:
- await hass.services.async_call(
- DOMAIN,
- SERVICE_EFFECT,
- {
- ATTR_EFFECT: "Rainbow",
- ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
- ATTR_INTENSITY: 200,
- ATTR_REVERSE: True,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- effect="Rainbow",
- intensity=200,
- reverse=True,
- segment_id=0,
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_EFFECT,
+ {
+ ATTR_EFFECT: "Rainbow",
+ ATTR_ENTITY_ID: "light.wled_rgb_light",
+ ATTR_INTENSITY: 200,
+ ATTR_REVERSE: True,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.segment.call_count == 6
+ mock_wled.segment.assert_called_with(
+ effect="Rainbow",
+ intensity=200,
+ reverse=True,
+ segment_id=0,
+ palette=None,
+ speed=None,
+ )
async def test_effect_service_error(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+ caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the WLED effect service."""
- aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400)
- await init_integration(hass, aioclient_mock)
+ mock_wled.segment.side_effect = WLEDError
- with patch("homeassistant.components.wled.WLED.update"):
- await hass.services.async_call(
- DOMAIN,
- SERVICE_EFFECT,
- {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_EFFECT: 9},
- blocking=True,
- )
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_EFFECT,
+ {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_EFFECT: 9},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
- state = hass.states.get("light.wled_rgb_light_segment_0")
- assert state.state == STATE_ON
- assert "Invalid response from API" in caplog.text
+ state = hass.states.get("light.wled_rgb_light")
+ assert state
+ assert state.state == STATE_ON
+ assert "Invalid response from API" in caplog.text
+ assert mock_wled.segment.call_count == 1
+ mock_wled.segment.assert_called_with(
+ effect=9, segment_id=0, intensity=None, palette=None, reverse=None, speed=None
+ )
async def test_preset_service(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock
) -> None:
"""Test the preset service of a WLED light."""
- await init_integration(hass, aioclient_mock)
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_PRESET,
+ {
+ ATTR_ENTITY_ID: "light.wled_rgb_light",
+ ATTR_PRESET: 1,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.preset.call_count == 1
+ mock_wled.preset.assert_called_with(preset=1)
- with patch("wled.WLED.preset") as light_mock:
- await hass.services.async_call(
- DOMAIN,
- SERVICE_PRESET,
- {
- ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
- ATTR_PRESET: 1,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- light_mock.assert_called_once_with(
- preset=1,
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_PRESET,
+ {
+ ATTR_ENTITY_ID: "light.wled_rgb_light_master",
+ ATTR_PRESET: 2,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.preset.call_count == 2
+ mock_wled.preset.assert_called_with(preset=2)
async def test_preset_service_error(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+ caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the WLED preset service."""
- aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400)
- await init_integration(hass, aioclient_mock)
+ mock_wled.preset.side_effect = WLEDError
- with patch("homeassistant.components.wled.WLED.update"):
- await hass.services.async_call(
- DOMAIN,
- SERVICE_PRESET,
- {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_PRESET: 1},
- blocking=True,
- )
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_PRESET,
+ {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_PRESET: 1},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
- state = hass.states.get("light.wled_rgb_light_segment_0")
- assert state.state == STATE_ON
- assert "Invalid response from API" in caplog.text
+ state = hass.states.get("light.wled_rgb_light")
+ assert state
+ assert state.state == STATE_ON
+ assert "Invalid response from API" in caplog.text
+ assert mock_wled.preset.call_count == 1
+ mock_wled.preset.assert_called_with(preset=1)
+
+
+@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True)
+async def test_single_segment_with_keep_master_light(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+) -> None:
+ """Test the behavior of the integration with a single segment."""
+ assert not hass.states.get("light.wled_rgb_light_master")
+
+ hass.config_entries.async_update_entry(
+ init_integration, options={CONF_KEEP_MASTER_LIGHT: True}
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.wled_rgb_light_master")
+ assert state
+ assert state.state == STATE_ON
diff --git a/tests/components/wled/test_select.py b/tests/components/wled/test_select.py
new file mode 100644
index 00000000000..abdef0c2ff5
--- /dev/null
+++ b/tests/components/wled/test_select.py
@@ -0,0 +1,380 @@
+"""Tests for the WLED select platform."""
+import json
+from unittest.mock import MagicMock
+
+import pytest
+from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError
+
+from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
+from homeassistant.components.select.const import ATTR_OPTION, ATTR_OPTIONS
+from homeassistant.components.wled.const import DOMAIN, SCAN_INTERVAL
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_ICON,
+ SERVICE_SELECT_OPTION,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+import homeassistant.util.dt as dt_util
+
+from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture
+
+
+@pytest.fixture
+async def enable_all(hass: HomeAssistant) -> None:
+ """Enable all disabled by default select entities."""
+ registry = er.async_get(hass)
+
+ # Pre-create registry entries for disabled by default sensors
+ registry.async_get_or_create(
+ SELECT_DOMAIN,
+ DOMAIN,
+ "aabbccddeeff_palette_0",
+ suggested_object_id="wled_rgb_light_color_palette",
+ disabled_by=None,
+ )
+
+ registry.async_get_or_create(
+ SELECT_DOMAIN,
+ DOMAIN,
+ "aabbccddeeff_palette_1",
+ suggested_object_id="wled_rgb_light_segment_1_color_palette",
+ disabled_by=None,
+ )
+
+
+async def test_color_palette_state(
+ hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry
+) -> None:
+ """Test the creation and values of the WLED selects."""
+ entity_registry = er.async_get(hass)
+
+ # First segment of the strip
+ state = hass.states.get("select.wled_rgb_light_segment_1_color_palette")
+ assert state
+ assert state.attributes.get(ATTR_ICON) == "mdi:palette-outline"
+ assert state.attributes.get(ATTR_OPTIONS) == [
+ "Analogous",
+ "April Night",
+ "Autumn",
+ "Based on Primary",
+ "Based on Set",
+ "Beach",
+ "Beech",
+ "Breeze",
+ "C9",
+ "Cloud",
+ "Cyane",
+ "Default",
+ "Departure",
+ "Drywet",
+ "Fire",
+ "Forest",
+ "Grintage",
+ "Hult",
+ "Hult 64",
+ "Icefire",
+ "Jul",
+ "Landscape",
+ "Lava",
+ "Light Pink",
+ "Magenta",
+ "Magred",
+ "Ocean",
+ "Orange & Teal",
+ "Orangery",
+ "Party",
+ "Pastel",
+ "Primary Color",
+ "Rainbow",
+ "Rainbow Bands",
+ "Random Cycle",
+ "Red & Blue",
+ "Rewhi",
+ "Rivendell",
+ "Sakura",
+ "Set Colors",
+ "Sherbet",
+ "Splash",
+ "Sunset",
+ "Sunset 2",
+ "Tertiary",
+ "Tiamat",
+ "Vintage",
+ "Yelblu",
+ "Yellowout",
+ "Yelmag",
+ ]
+ assert state.state == "Random Cycle"
+
+ entry = entity_registry.async_get("select.wled_rgb_light_segment_1_color_palette")
+ assert entry
+ assert entry.unique_id == "aabbccddeeff_palette_1"
+
+
+async def test_color_palette_segment_change_state(
+ hass: HomeAssistant,
+ enable_all: None,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+) -> None:
+ """Test the option change of state of the WLED segments."""
+ await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette",
+ ATTR_OPTION: "Some Other Palette",
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.segment.call_count == 1
+ mock_wled.segment.assert_called_with(
+ segment_id=1,
+ palette="Some Other Palette",
+ )
+
+
+@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True)
+async def test_color_palette_dynamically_handle_segments(
+ hass: HomeAssistant,
+ enable_all: None,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+) -> None:
+ """Test if a new/deleted segment is dynamically added/removed."""
+ segment0 = hass.states.get("select.wled_rgb_light_color_palette")
+ segment1 = hass.states.get("select.wled_rgb_light_segment_1_color_palette")
+ assert segment0
+ assert segment0.state == "Default"
+ assert not segment1
+
+ return_value = mock_wled.update.return_value
+ mock_wled.update.return_value = WLEDDevice(
+ json.loads(load_fixture("wled/rgb.json"))
+ )
+
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ segment0 = hass.states.get("select.wled_rgb_light_color_palette")
+ segment1 = hass.states.get("select.wled_rgb_light_segment_1_color_palette")
+ assert segment0
+ assert segment0.state == "Default"
+ assert segment1
+ assert segment1.state == "Random Cycle"
+
+ # Test adding if segment shows up again, including the master entity
+ mock_wled.update.return_value = return_value
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ segment0 = hass.states.get("select.wled_rgb_light_color_palette")
+ segment1 = hass.states.get("select.wled_rgb_light_segment_1_color_palette")
+ assert segment0
+ assert segment0.state == "Default"
+ assert segment1
+ assert segment1.state == STATE_UNAVAILABLE
+
+
+async def test_color_palette_select_error(
+ hass: HomeAssistant,
+ enable_all: None,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ """Test error handling of the WLED selects."""
+ mock_wled.segment.side_effect = WLEDError
+
+ await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette",
+ ATTR_OPTION: "Whatever",
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("select.wled_rgb_light_segment_1_color_palette")
+ assert state
+ assert state.state == "Random Cycle"
+ assert "Invalid response from API" in caplog.text
+ assert mock_wled.segment.call_count == 1
+ mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever")
+
+
+async def test_color_palette_select_connection_error(
+ hass: HomeAssistant,
+ enable_all: None,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ """Test error handling of the WLED selects."""
+ mock_wled.segment.side_effect = WLEDConnectionError
+
+ await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette",
+ ATTR_OPTION: "Whatever",
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("select.wled_rgb_light_segment_1_color_palette")
+ assert state
+ assert state.state == STATE_UNAVAILABLE
+ assert "Error communicating with API" in caplog.text
+ assert mock_wled.segment.call_count == 1
+ mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever")
+
+
+async def test_preset_unavailable_without_presets(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+) -> None:
+ """Test WLED preset entity is unavailable when presets are not available."""
+ state = hass.states.get("select.wled_rgb_light_preset")
+ assert state
+ assert state.state == STATE_UNAVAILABLE
+
+
+@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
+async def test_preset_state(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+) -> None:
+ """Test the creation and values of the WLED selects."""
+ entity_registry = er.async_get(hass)
+
+ state = hass.states.get("select.wled_rgbw_light_preset")
+ assert state
+ assert state.attributes.get(ATTR_ICON) == "mdi:playlist-play"
+ assert state.attributes.get(ATTR_OPTIONS) == ["Preset 1", "Preset 2"]
+ assert state.state == "Preset 1"
+
+ entry = entity_registry.async_get("select.wled_rgbw_light_preset")
+ assert entry
+ assert entry.unique_id == "aabbccddee11_preset"
+
+ await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.wled_rgbw_light_preset",
+ ATTR_OPTION: "Preset 2",
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.preset.call_count == 1
+ mock_wled.preset.assert_called_with(preset="Preset 2")
+
+
+@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
+async def test_old_style_preset_active(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ """Test unknown preset returned (when old style/unknown) preset is active."""
+ # Set device preset state to a random number
+ mock_wled.update.return_value.state.preset = 99
+
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("select.wled_rgbw_light_preset")
+ assert state
+ assert state.state == STATE_UNKNOWN
+
+
+@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
+async def test_preset_select_error(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ """Test error handling of the WLED selects."""
+ mock_wled.preset.side_effect = WLEDError
+
+ await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.wled_rgbw_light_preset",
+ ATTR_OPTION: "Preset 2",
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("select.wled_rgbw_light_preset")
+ assert state
+ assert state.state == "Preset 1"
+ assert "Invalid response from API" in caplog.text
+ assert mock_wled.preset.call_count == 1
+ mock_wled.preset.assert_called_with(preset="Preset 2")
+
+
+@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
+async def test_preset_select_connection_error(
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ """Test error handling of the WLED selects."""
+ mock_wled.preset.side_effect = WLEDConnectionError
+
+ await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.wled_rgbw_light_preset",
+ ATTR_OPTION: "Preset 2",
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("select.wled_rgbw_light_preset")
+ assert state
+ assert state.state == STATE_UNAVAILABLE
+ assert "Error communicating with API" in caplog.text
+ assert mock_wled.preset.call_count == 1
+ mock_wled.preset.assert_called_with(preset="Preset 2")
+
+
+@pytest.mark.parametrize(
+ "entity_id",
+ (
+ "select.wled_rgb_light_color_palette",
+ "select.wled_rgb_light_segment_1_color_palette",
+ ),
+)
+async def test_disabled_by_default_selects(
+ hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str
+) -> None:
+ """Test the disabled by default WLED selects."""
+ registry = er.async_get(hass)
+
+ 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 == er.DISABLED_INTEGRATION
diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py
index fcd36dd70a9..4f2b07f4f51 100644
--- a/tests/components/wled/test_sensor.py
+++ b/tests/components/wled/test_sensor.py
@@ -1,6 +1,6 @@
"""Tests for the WLED sensor platform."""
from datetime import datetime
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
import pytest
@@ -23,21 +23,21 @@ from homeassistant.const import (
DATA_BYTES,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+ STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
-from tests.components.wled import init_integration
-from tests.test_util.aiohttp import AiohttpClientMocker
+from tests.common import MockConfigEntry
async def test_sensors(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+ mock_wled: MagicMock,
) -> None:
"""Test the creation and values of the WLED sensors."""
-
- entry = await init_integration(hass, aioclient_mock, skip_setup=True)
registry = er.async_get(hass)
# Pre-create registry entries for disabled by default sensors
@@ -90,9 +90,10 @@ async def test_sensors(
)
# Setup
+ mock_config_entry.add_to_hass(hass)
test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=dt_util.UTC)
with patch("homeassistant.components.wled.sensor.utcnow", return_value=test_time):
- await hass.config_entries.async_setup(entry.entry_id)
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.wled_rgb_light_estimated_current")
@@ -184,10 +185,9 @@ async def test_sensors(
),
)
async def test_disabled_by_default_sensors(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, entity_id: str
+ hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str
) -> None:
"""Test the disabled by default WLED sensors."""
- await init_integration(hass, aioclient_mock)
registry = er.async_get(hass)
state = hass.states.get(entity_id)
@@ -197,3 +197,44 @@ async def test_disabled_by_default_sensors(
assert entry
assert entry.disabled
assert entry.disabled_by == er.DISABLED_INTEGRATION
+
+
+@pytest.mark.parametrize(
+ "key",
+ [
+ "bssid",
+ "channel",
+ "rssi",
+ "signal",
+ ],
+)
+async def test_no_wifi_support(
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+ mock_wled: MagicMock,
+ key: str,
+) -> None:
+ """Test missing Wi-Fi information from WLED device."""
+ registry = er.async_get(hass)
+
+ # Pre-create registry entries for disabled by default sensors
+ registry.async_get_or_create(
+ SENSOR_DOMAIN,
+ DOMAIN,
+ f"aabbccddeeff_wifi_{key}",
+ suggested_object_id=f"wled_rgb_light_wifi_{key}",
+ disabled_by=None,
+ )
+
+ # Remove Wi-Fi info
+ device = mock_wled.update.return_value
+ device.info.wifi = None
+
+ # Setup
+ mock_config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(f"sensor.wled_rgb_light_wifi_{key}")
+ assert state
+ assert state.state == STATE_UNKNOWN
diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py
index ddeeee41ac8..eb8b8b526e0 100644
--- a/tests/components/wled/test_switch.py
+++ b/tests/components/wled/test_switch.py
@@ -1,7 +1,8 @@
"""Tests for the WLED switch platform."""
-from unittest.mock import patch
+from unittest.mock import MagicMock
-from wled import WLEDConnectionError
+import pytest
+from wled import WLEDConnectionError, WLEDError
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.wled.const import (
@@ -22,16 +23,13 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
-from tests.components.wled import init_integration
-from tests.test_util.aiohttp import AiohttpClientMocker
+from tests.common import MockConfigEntry
async def test_switch_state(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant, init_integration: MockConfigEntry
) -> None:
"""Test the creation and values of the WLED switches."""
- await init_integration(hass, aioclient_mock)
-
entity_registry = er.async_get(hass)
state = hass.states.get("switch.wled_rgb_light_nightlight")
@@ -68,112 +66,115 @@ async def test_switch_state(
async def test_switch_change_state(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock
) -> None:
"""Test the change of state of the WLED switches."""
- await init_integration(hass, aioclient_mock)
# Nightlight
- with patch("wled.WLED.nightlight") as nightlight_mock:
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"},
- blocking=True,
- )
- await hass.async_block_till_done()
- nightlight_mock.assert_called_once_with(on=True)
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.nightlight.call_count == 1
+ mock_wled.nightlight.assert_called_with(on=True)
- with patch("wled.WLED.nightlight") as nightlight_mock:
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"},
- blocking=True,
- )
- await hass.async_block_till_done()
- nightlight_mock.assert_called_once_with(on=False)
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.nightlight.call_count == 2
+ mock_wled.nightlight.assert_called_with(on=False)
# Sync send
- with patch("wled.WLED.sync") as sync_mock:
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"},
- blocking=True,
- )
- await hass.async_block_till_done()
- sync_mock.assert_called_once_with(send=True)
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.sync.call_count == 1
+ mock_wled.sync.assert_called_with(send=True)
- with patch("wled.WLED.sync") as sync_mock:
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"},
- blocking=True,
- )
- await hass.async_block_till_done()
- sync_mock.assert_called_once_with(send=False)
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.sync.call_count == 2
+ mock_wled.sync.assert_called_with(send=False)
# Sync receive
- with patch("wled.WLED.sync") as sync_mock:
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"},
- blocking=True,
- )
- await hass.async_block_till_done()
- sync_mock.assert_called_once_with(receive=False)
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.sync.call_count == 3
+ mock_wled.sync.assert_called_with(receive=False)
- with patch("wled.WLED.sync") as sync_mock:
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"},
- blocking=True,
- )
- await hass.async_block_till_done()
- sync_mock.assert_called_once_with(receive=True)
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert mock_wled.sync.call_count == 4
+ mock_wled.sync.assert_called_with(receive=True)
async def test_switch_error(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+ caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the WLED switches."""
- aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400)
- await init_integration(hass, aioclient_mock)
+ mock_wled.nightlight.side_effect = WLEDError
- with patch("homeassistant.components.wled.WLED.update"):
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"},
- blocking=True,
- )
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
- state = hass.states.get("switch.wled_rgb_light_nightlight")
- assert state.state == STATE_OFF
- assert "Invalid response from API" in caplog.text
+ state = hass.states.get("switch.wled_rgb_light_nightlight")
+ assert state
+ assert state.state == STATE_OFF
+ assert "Invalid response from API" in caplog.text
async def test_switch_connection_error(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+ hass: HomeAssistant,
+ init_integration: MockConfigEntry,
+ mock_wled: MagicMock,
+ caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the WLED switches."""
- await init_integration(hass, aioclient_mock)
+ mock_wled.nightlight.side_effect = WLEDConnectionError
- with patch("homeassistant.components.wled.WLED.update"), patch(
- "homeassistant.components.wled.WLED.nightlight", side_effect=WLEDConnectionError
- ):
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"},
- blocking=True,
- )
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
- state = hass.states.get("switch.wled_rgb_light_nightlight")
- assert state.state == STATE_UNAVAILABLE
+ state = hass.states.get("switch.wled_rgb_light_nightlight")
+ assert state
+ assert state.state == STATE_UNAVAILABLE
+ assert "Error communicating with API" in caplog.text
diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py
index 859338b82d3..3f445a1fdec 100644
--- a/tests/components/xiaomi_aqara/test_config_flow.py
+++ b/tests/components/xiaomi_aqara/test_config_flow.py
@@ -113,7 +113,7 @@ async def test_config_flow_user_success(hass):
async def test_config_flow_user_multiple_success(hass):
- """Test a successful config flow initialized by the user with multiple gateways discoverd."""
+ """Test a successful config flow initialized by the user with multiple gateways discovered."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
@@ -249,7 +249,7 @@ async def test_config_flow_user_host_mac_success(hass):
async def test_config_flow_user_discovery_error(hass):
- """Test a failed config flow initialized by the user with no gateways discoverd."""
+ """Test a failed config flow initialized by the user with no gateways discovered."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py
index de1ccbf1a8b..68091efffa1 100644
--- a/tests/components/xiaomi_miio/test_config_flow.py
+++ b/tests/components/xiaomi_miio/test_config_flow.py
@@ -2,27 +2,86 @@
from unittest.mock import Mock, patch
from miio import DeviceException
+import pytest
-from homeassistant import config_entries
+from homeassistant import config_entries, data_entry_flow
from homeassistant.components.xiaomi_miio import const
-from homeassistant.components.xiaomi_miio.config_flow import DEFAULT_GATEWAY_NAME
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
+from tests.common import MockConfigEntry
+
ZEROCONF_NAME = "name"
ZEROCONF_PROP = "properties"
ZEROCONF_MAC = "mac"
TEST_HOST = "1.2.3.4"
+TEST_HOST2 = "5.6.7.8"
+TEST_CLOUD_USER = "username"
+TEST_CLOUD_PASS = "password"
+TEST_CLOUD_COUNTRY = "cn"
TEST_TOKEN = "12345678901234567890123456789012"
TEST_NAME = "Test_Gateway"
+TEST_NAME2 = "Test_Gateway_2"
TEST_MODEL = const.MODELS_GATEWAY[0]
TEST_MAC = "ab:cd:ef:gh:ij:kl"
+TEST_MAC2 = "mn:op:qr:st:uv:wx"
TEST_MAC_DEVICE = "abcdefghijkl"
+TEST_MAC_DEVICE2 = "mnopqrstuvwx"
TEST_GATEWAY_ID = TEST_MAC
TEST_HARDWARE_VERSION = "AB123"
TEST_FIRMWARE_VERSION = "1.2.3_456"
TEST_ZEROCONF_NAME = "lumi-gateway-v3_miio12345678._miio._udp.local."
TEST_SUB_DEVICE_LIST = []
+TEST_CLOUD_DEVICES_1 = [
+ {
+ "parent_id": None,
+ "name": TEST_NAME,
+ "model": TEST_MODEL,
+ "localip": TEST_HOST,
+ "mac": TEST_MAC_DEVICE,
+ "token": TEST_TOKEN,
+ }
+]
+TEST_CLOUD_DEVICES_2 = [
+ {
+ "parent_id": None,
+ "name": TEST_NAME,
+ "model": TEST_MODEL,
+ "localip": TEST_HOST,
+ "mac": TEST_MAC_DEVICE,
+ "token": TEST_TOKEN,
+ },
+ {
+ "parent_id": None,
+ "name": TEST_NAME2,
+ "model": TEST_MODEL,
+ "localip": TEST_HOST2,
+ "mac": TEST_MAC_DEVICE2,
+ "token": TEST_TOKEN,
+ },
+]
+
+
+@pytest.fixture(name="xiaomi_miio_connect", autouse=True)
+def xiaomi_miio_connect_fixture():
+ """Mock denonavr connection and entry setup."""
+ mock_info = get_mock_info()
+
+ with patch(
+ "homeassistant.components.xiaomi_miio.device.Device.info",
+ return_value=mock_info,
+ ), patch(
+ "homeassistant.components.xiaomi_miio.config_flow.MiCloud.login",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices",
+ return_value=TEST_CLOUD_DEVICES_1,
+ ), patch(
+ "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
+ ), patch(
+ "homeassistant.components.xiaomi_miio.async_unload_entry", return_value=True
+ ):
+ yield
def get_mock_info(
@@ -48,7 +107,16 @@ async def test_config_flow_step_gateway_connect_error(hass):
)
assert result["type"] == "form"
- assert result["step_id"] == "device"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {const.CONF_MANUAL: True},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "manual"
assert result["errors"] == {}
with patch(
@@ -61,7 +129,7 @@ async def test_config_flow_step_gateway_connect_error(hass):
)
assert result["type"] == "form"
- assert result["step_id"] == "device"
+ assert result["step_id"] == "connect"
assert result["errors"] == {"base": "cannot_connect"}
@@ -72,26 +140,30 @@ async def test_config_flow_gateway_success(hass):
)
assert result["type"] == "form"
- assert result["step_id"] == "device"
+ assert result["step_id"] == "cloud"
assert result["errors"] == {}
- mock_info = get_mock_info()
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {const.CONF_MANUAL: True},
+ )
- with patch(
- "homeassistant.components.xiaomi_miio.device.Device.info",
- return_value=mock_info,
- ), patch(
- "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
- ):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
- )
+ assert result["type"] == "form"
+ assert result["step_id"] == "manual"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
+ )
assert result["type"] == "create_entry"
- assert result["title"] == DEFAULT_GATEWAY_NAME
+ assert result["title"] == TEST_MODEL
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
+ const.CONF_CLOUD_USERNAME: None,
+ const.CONF_CLOUD_PASSWORD: None,
+ const.CONF_CLOUD_COUNTRY: None,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: TEST_MODEL,
@@ -99,6 +171,202 @@ async def test_config_flow_gateway_success(hass):
}
+async def test_config_flow_gateway_cloud_success(hass):
+ """Test a successful config flow using cloud."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+ },
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == TEST_NAME
+ assert result["data"] == {
+ const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+ CONF_HOST: TEST_HOST,
+ CONF_TOKEN: TEST_TOKEN,
+ const.CONF_MODEL: TEST_MODEL,
+ const.CONF_MAC: TEST_MAC,
+ }
+
+
+async def test_config_flow_gateway_cloud_multiple_success(hass):
+ """Test a successful config flow using cloud with multiple devices."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices",
+ return_value=TEST_CLOUD_DEVICES_2,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "select"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"select_device": f"{TEST_NAME2} - {TEST_MODEL}"},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == TEST_NAME2
+ assert result["data"] == {
+ const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+ CONF_HOST: TEST_HOST2,
+ CONF_TOKEN: TEST_TOKEN,
+ const.CONF_MODEL: TEST_MODEL,
+ const.CONF_MAC: TEST_MAC2,
+ }
+
+
+async def test_config_flow_gateway_cloud_incomplete(hass):
+ """Test a failed config flow using incomplete cloud credentials."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {"base": "cloud_credentials_incomplete"}
+
+
+async def test_config_flow_gateway_cloud_login_error(hass):
+ """Test a failed config flow using cloud login error."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.xiaomi_miio.config_flow.MiCloud.login",
+ return_value=False,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {"base": "cloud_login_error"}
+
+
+async def test_config_flow_gateway_cloud_no_devices(hass):
+ """Test a failed config flow using cloud with no devices."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices",
+ return_value=[],
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {"base": "cloud_no_devices"}
+
+
+async def test_config_flow_gateway_cloud_missing_token(hass):
+ """Test a failed config flow using cloud with a missing token."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ cloud_device = [
+ {
+ "parent_id": None,
+ "name": TEST_NAME,
+ "model": TEST_MODEL,
+ "localip": TEST_HOST,
+ "mac": TEST_MAC_DEVICE,
+ "token": None,
+ }
+ ]
+
+ with patch(
+ "homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices",
+ return_value=cloud_device,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+ },
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "incomplete_info"
+
+
async def test_zeroconf_gateway_success(hass):
"""Test a successful zeroconf discovery of a gateway."""
result = await hass.config_entries.flow.async_init(
@@ -112,26 +380,25 @@ async def test_zeroconf_gateway_success(hass):
)
assert result["type"] == "form"
- assert result["step_id"] == "device"
+ assert result["step_id"] == "cloud"
assert result["errors"] == {}
- mock_info = get_mock_info()
-
- with patch(
- "homeassistant.components.xiaomi_miio.device.Device.info",
- return_value=mock_info,
- ), patch(
- "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
- ):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {CONF_TOKEN: TEST_TOKEN},
- )
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+ },
+ )
assert result["type"] == "create_entry"
- assert result["title"] == DEFAULT_GATEWAY_NAME
+ assert result["title"] == TEST_NAME
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: TEST_MODEL,
@@ -184,7 +451,16 @@ async def test_config_flow_step_device_connect_error(hass):
)
assert result["type"] == "form"
- assert result["step_id"] == "device"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {const.CONF_MANUAL: True},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "manual"
assert result["errors"] == {}
with patch(
@@ -197,7 +473,7 @@ async def test_config_flow_step_device_connect_error(hass):
)
assert result["type"] == "form"
- assert result["step_id"] == "device"
+ assert result["step_id"] == "connect"
assert result["errors"] == {"base": "cannot_connect"}
@@ -208,7 +484,16 @@ async def test_config_flow_step_unknown_device(hass):
)
assert result["type"] == "form"
- assert result["step_id"] == "device"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {const.CONF_MANUAL: True},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "manual"
assert result["errors"] == {}
mock_info = get_mock_info(model="UNKNOWN")
@@ -223,7 +508,7 @@ async def test_config_flow_step_unknown_device(hass):
)
assert result["type"] == "form"
- assert result["step_id"] == "device"
+ assert result["step_id"] == "connect"
assert result["errors"] == {"base": "unknown_device"}
@@ -234,8 +519,6 @@ async def test_import_flow_success(hass):
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
- ), patch(
- "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
@@ -247,6 +530,9 @@ async def test_import_flow_success(hass):
assert result["title"] == TEST_NAME
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_DEVICE,
+ const.CONF_CLOUD_USERNAME: None,
+ const.CONF_CLOUD_PASSWORD: None,
+ const.CONF_CLOUD_COUNTRY: None,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: const.MODELS_SWITCH[0],
@@ -261,7 +547,16 @@ async def test_config_flow_step_device_manual_model_succes(hass):
)
assert result["type"] == "form"
- assert result["step_id"] == "device"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {const.CONF_MANUAL: True},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "manual"
assert result["errors"] == {}
with patch(
@@ -274,7 +569,7 @@ async def test_config_flow_step_device_manual_model_succes(hass):
)
assert result["type"] == "form"
- assert result["step_id"] == "device"
+ assert result["step_id"] == "connect"
assert result["errors"] == {"base": "cannot_connect"}
overwrite_model = const.MODELS_VACUUM[0]
@@ -282,18 +577,19 @@ async def test_config_flow_step_device_manual_model_succes(hass):
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
side_effect=DeviceException({}),
- ), patch(
- "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: overwrite_model},
+ {const.CONF_MODEL: overwrite_model},
)
assert result["type"] == "create_entry"
assert result["title"] == overwrite_model
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_DEVICE,
+ const.CONF_CLOUD_USERNAME: None,
+ const.CONF_CLOUD_PASSWORD: None,
+ const.CONF_CLOUD_COUNTRY: None,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: overwrite_model,
@@ -308,7 +604,16 @@ async def config_flow_device_success(hass, model_to_test):
)
assert result["type"] == "form"
- assert result["step_id"] == "device"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {const.CONF_MANUAL: True},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "manual"
assert result["errors"] == {}
mock_info = get_mock_info(model=model_to_test)
@@ -316,8 +621,6 @@ async def config_flow_device_success(hass, model_to_test):
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
- ), patch(
- "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -328,6 +631,9 @@ async def config_flow_device_success(hass, model_to_test):
assert result["title"] == model_to_test
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_DEVICE,
+ const.CONF_CLOUD_USERNAME: None,
+ const.CONF_CLOUD_PASSWORD: None,
+ const.CONF_CLOUD_COUNTRY: None,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: model_to_test,
@@ -348,7 +654,16 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test):
)
assert result["type"] == "form"
- assert result["step_id"] == "device"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {const.CONF_MANUAL: True},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "manual"
assert result["errors"] == {}
mock_info = get_mock_info(model=model_to_test)
@@ -356,8 +671,6 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test):
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
- ), patch(
- "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -368,6 +681,9 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test):
assert result["title"] == model_to_test
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_DEVICE,
+ const.CONF_CLOUD_USERNAME: None,
+ const.CONF_CLOUD_PASSWORD: None,
+ const.CONF_CLOUD_COUNTRY: None,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: model_to_test,
@@ -399,3 +715,147 @@ async def test_zeroconf_vacuum_success(hass):
test_vacuum_model = const.MODELS_VACUUM[0]
test_zeroconf_name = const.MODELS_VACUUM[0].replace(".", "-")
await zeroconf_device_success(hass, test_zeroconf_name, test_vacuum_model)
+
+
+async def test_options_flow(hass):
+ """Test specifying non default settings using options flow."""
+ config_entry = MockConfigEntry(
+ domain=const.DOMAIN,
+ unique_id=TEST_GATEWAY_ID,
+ data={
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+ const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
+ CONF_HOST: TEST_HOST,
+ CONF_TOKEN: TEST_TOKEN,
+ const.CONF_MODEL: TEST_MODEL,
+ const.CONF_MAC: TEST_MAC,
+ },
+ title=TEST_NAME,
+ )
+ 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"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ const.CONF_CLOUD_SUBDEVICES: True,
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert config_entry.options == {
+ const.CONF_CLOUD_SUBDEVICES: True,
+ }
+
+
+async def test_options_flow_incomplete(hass):
+ """Test specifying incomplete settings using options flow."""
+ config_entry = MockConfigEntry(
+ domain=const.DOMAIN,
+ unique_id=TEST_GATEWAY_ID,
+ data={
+ const.CONF_CLOUD_USERNAME: None,
+ const.CONF_CLOUD_PASSWORD: None,
+ const.CONF_CLOUD_COUNTRY: None,
+ const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
+ CONF_HOST: TEST_HOST,
+ CONF_TOKEN: TEST_TOKEN,
+ const.CONF_MODEL: TEST_MODEL,
+ const.CONF_MAC: TEST_MAC,
+ },
+ title=TEST_NAME,
+ )
+ 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"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ const.CONF_CLOUD_SUBDEVICES: True,
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+ assert result["errors"] == {"base": "cloud_credentials_incomplete"}
+
+
+async def test_reauth(hass):
+ """Test a reauth flow."""
+ # await setup.async_setup_component(hass, "persistent_notification", {})
+ config_entry = MockConfigEntry(
+ domain=const.DOMAIN,
+ unique_id=TEST_GATEWAY_ID,
+ data={
+ const.CONF_CLOUD_USERNAME: None,
+ const.CONF_CLOUD_PASSWORD: None,
+ const.CONF_CLOUD_COUNTRY: None,
+ const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
+ CONF_HOST: TEST_HOST,
+ CONF_TOKEN: TEST_TOKEN,
+ const.CONF_MODEL: TEST_MODEL,
+ const.CONF_MAC: TEST_MAC,
+ },
+ title=TEST_NAME,
+ )
+ 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.flow.async_init(
+ const.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(
+ result["flow_id"],
+ {},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "cloud"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+ },
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "reauth_successful"
+
+ config_data = config_entry.data.copy()
+ assert config_data == {
+ const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
+ const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+ const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+ const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+ CONF_HOST: TEST_HOST,
+ CONF_TOKEN: TEST_TOKEN,
+ const.CONF_MODEL: TEST_MODEL,
+ const.CONF_MAC: TEST_MAC,
+ }
diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py
index fe0466472fa..0eb806c0a64 100644
--- a/tests/components/xiaomi_miio/test_vacuum.py
+++ b/tests/components/xiaomi_miio/test_vacuum.py
@@ -115,8 +115,8 @@ def mirobo_is_got_error_fixture():
mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2]
- with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vaccum_cls:
- mock_vaccum_cls.return_value = mock_vacuum
+ with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vacuum_cls:
+ mock_vacuum_cls.return_value = mock_vacuum
yield mock_vacuum
@@ -143,8 +143,8 @@ def mirobo_old_speeds_fixture(request):
mock_vacuum.fan_speed_presets.return_value = request.param
mock_vacuum.status().fanspeed = list(request.param.values())[0]
- with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vaccum_cls:
- mock_vaccum_cls.return_value = mock_vacuum
+ with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vacuum_cls:
+ mock_vacuum_cls.return_value = mock_vacuum
yield mock_vacuum
@@ -189,8 +189,8 @@ def mirobo_is_on_fixture():
mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2]
- with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vaccum_cls:
- mock_vaccum_cls.return_value = mock_vacuum
+ with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vacuum_cls:
+ mock_vacuum_cls.return_value = mock_vacuum
yield mock_vacuum
diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py
index 84a9e475c32..45624ae0a8b 100644
--- a/tests/components/yamaha/test_media_player.py
+++ b/tests/components/yamaha/test_media_player.py
@@ -130,6 +130,32 @@ async def test_enable_output(hass, device, main_zone):
assert main_zone.enable_output.call_args == call(port, enabled)
+@pytest.mark.parametrize(
+ "cursor,method",
+ [
+ (yamaha.CURSOR_TYPE_DOWN, "menu_down"),
+ (yamaha.CURSOR_TYPE_LEFT, "menu_left"),
+ (yamaha.CURSOR_TYPE_RETURN, "menu_return"),
+ (yamaha.CURSOR_TYPE_RIGHT, "menu_right"),
+ (yamaha.CURSOR_TYPE_SELECT, "menu_sel"),
+ (yamaha.CURSOR_TYPE_UP, "menu_up"),
+ ],
+)
+@pytest.mark.usefixtures("device")
+async def test_menu_cursor(hass, main_zone, cursor, method):
+ """Verify that the correct menu method is called for the menu_cursor service."""
+ assert await async_setup_component(hass, mp.DOMAIN, CONFIG)
+ await hass.async_block_till_done()
+
+ data = {
+ "entity_id": "media_player.yamaha_receiver_main_zone",
+ "cursor": cursor,
+ }
+ await hass.services.async_call(DOMAIN, yamaha.SERVICE_MENU_CURSOR, data, True)
+
+ getattr(main_zone, method).assert_called_once_with()
+
+
async def test_select_scene(hass, device, main_zone, caplog):
"""Test select scene service."""
scene_prop = PropertyMock(return_value=None)
diff --git a/tests/components/yamaha_musiccast/__init__.py b/tests/components/yamaha_musiccast/__init__.py
new file mode 100644
index 00000000000..d5b10774c10
--- /dev/null
+++ b/tests/components/yamaha_musiccast/__init__.py
@@ -0,0 +1 @@
+"""Tests for the MusicCast integration."""
diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py
new file mode 100644
index 00000000000..6f5709ec7cc
--- /dev/null
+++ b/tests/components/yamaha_musiccast/test_config_flow.py
@@ -0,0 +1,287 @@
+"""Test config flow."""
+
+from unittest.mock import patch
+
+from aiomusiccast import MusicCastConnectionException
+import pytest
+
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components import ssdp
+from homeassistant.components.yamaha_musiccast.const import DOMAIN
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST
+
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture(autouse=True)
+def mock_setup_entry():
+ """Mock setting up a config entry."""
+ with patch(
+ "homeassistant.components.yamaha_musiccast.async_setup_entry", return_value=True
+ ):
+ yield
+
+
+@pytest.fixture
+def mock_get_device_info_valid():
+ """Mock getting valid device info from musiccast API."""
+ with patch(
+ "aiomusiccast.MusicCastDevice.get_device_info",
+ return_value={"system_id": "1234567890", "model_name": "MC20"},
+ ):
+ yield
+
+
+@pytest.fixture
+def mock_get_device_info_invalid():
+ """Mock getting invalid device info from musiccast API."""
+ with patch(
+ "aiomusiccast.MusicCastDevice.get_device_info",
+ return_value={"type": "no_yamaha"},
+ ):
+ yield
+
+
+@pytest.fixture
+def mock_get_device_info_exception():
+ """Mock raising an unexpected Exception."""
+ with patch(
+ "aiomusiccast.MusicCastDevice.get_device_info",
+ side_effect=Exception("mocked error"),
+ ):
+ yield
+
+
+@pytest.fixture
+def mock_get_device_info_mc_exception():
+ """Mock raising an unexpected Exception."""
+ with patch(
+ "aiomusiccast.MusicCastDevice.get_device_info",
+ side_effect=MusicCastConnectionException("mocked error"),
+ ):
+ yield
+
+
+@pytest.fixture
+def mock_ssdp_yamaha():
+ """Mock that the SSDP detected device is a musiccast device."""
+ with patch("aiomusiccast.MusicCastDevice.check_yamaha_ssdp", return_value=True):
+ yield
+
+
+@pytest.fixture
+def mock_ssdp_no_yamaha():
+ """Mock that the SSDP detected device is not a musiccast device."""
+ with patch("aiomusiccast.MusicCastDevice.check_yamaha_ssdp", return_value=False):
+ yield
+
+
+# User Flows
+
+
+async def test_user_input_device_not_found(hass, mock_get_device_info_mc_exception):
+ """Test when user specifies a non-existing device."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"host": "none"},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_user_input_non_yamaha_device_found(hass, mock_get_device_info_invalid):
+ """Test when user specifies an existing device, which does not provide the musiccast API."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"host": "127.0.0.1"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "no_musiccast_device"}
+
+
+async def test_user_input_device_already_existing(hass, mock_get_device_info_valid):
+ """Test when user specifies an existing device."""
+ mock_entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="1234567890",
+ data={CONF_HOST: "192.168.188.18", "model": "MC20", "serial": "1234567890"},
+ )
+ mock_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"host": "192.168.188.18"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "already_configured"
+
+
+async def test_user_input_unknown_error(hass, mock_get_device_info_exception):
+ """Test when user specifies an existing device, which does not provide the musiccast API."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"host": "127.0.0.1"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_user_input_device_found(hass, mock_get_device_info_valid):
+ """Test when user specifies an existing device."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"host": "127.0.0.1"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert isinstance(result2["result"], ConfigEntry)
+ assert result2["data"] == {
+ "host": "127.0.0.1",
+ "serial": "1234567890",
+ }
+
+
+async def test_import_device_already_existing(hass, mock_get_device_info_valid):
+ """Test when the configurations.yaml contains an existing device."""
+ mock_entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="1234567890",
+ data={CONF_HOST: "192.168.188.18", "model": "MC20", "serial": "1234567890"},
+ )
+ mock_entry.add_to_hass(hass)
+
+ config = {"platform": "yamaha_musiccast", "host": "192.168.188.18", "port": 5006}
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_import_error(hass, mock_get_device_info_exception):
+ """Test when in the configuration.yaml a device is configured, which cannot be added.."""
+ config = {"platform": "yamaha_musiccast", "host": "192.168.188.18", "port": 5006}
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "unknown"}
+
+
+async def test_import_device_successful(hass, mock_get_device_info_valid):
+ """Test when the device was imported successfully."""
+ config = {"platform": "yamaha_musiccast", "host": "127.0.0.1", "port": 5006}
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert isinstance(result["result"], ConfigEntry)
+ assert result["data"] == {
+ "host": "127.0.0.1",
+ "serial": "1234567890",
+ }
+
+
+# SSDP Flows
+
+
+async def test_ssdp_discovery_failed(hass, mock_ssdp_no_yamaha):
+ """Test when an SSDP discovered device is not a musiccast device."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_SSDP},
+ data={
+ ssdp.ATTR_SSDP_LOCATION: "http://127.0.0.1/desc.xml",
+ ssdp.ATTR_UPNP_MODEL_NAME: "MC20",
+ ssdp.ATTR_UPNP_SERIAL: "123456789",
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "yxc_control_url_missing"
+
+
+async def test_ssdp_discovery_successful_add_device(hass, mock_ssdp_yamaha):
+ """Test when the SSDP discovered device is a musiccast device and the user confirms it."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_SSDP},
+ data={
+ ssdp.ATTR_SSDP_LOCATION: "http://127.0.0.1/desc.xml",
+ ssdp.ATTR_UPNP_MODEL_NAME: "MC20",
+ ssdp.ATTR_UPNP_SERIAL: "1234567890",
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] is None
+ assert result["step_id"] == "confirm"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert isinstance(result2["result"], ConfigEntry)
+ assert result2["data"] == {
+ "host": "127.0.0.1",
+ "serial": "1234567890",
+ }
+
+
+async def test_ssdp_discovery_existing_device_update(hass, mock_ssdp_yamaha):
+ """Test when the SSDP discovered device is a musiccast device, but it already exists with another IP."""
+ mock_entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="1234567890",
+ data={CONF_HOST: "192.168.188.18", "model": "MC20", "serial": "1234567890"},
+ )
+ mock_entry.add_to_hass(hass)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_SSDP},
+ data={
+ ssdp.ATTR_SSDP_LOCATION: "http://127.0.0.1/desc.xml",
+ ssdp.ATTR_UPNP_MODEL_NAME: "MC20",
+ ssdp.ATTR_UPNP_SERIAL: "1234567890",
+ },
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+ assert mock_entry.data[CONF_HOST] == "127.0.0.1"
diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py
index 38c28a9a900..5725880f942 100644
--- a/tests/components/yeelight/__init__.py
+++ b/tests/components/yeelight/__init__.py
@@ -49,7 +49,9 @@ PROPERTIES = {
"bg_flowing": "0",
"bg_ct": "5000",
"bg_bright": "80",
- "bg_rgb": "16711680",
+ "bg_rgb": "65280",
+ "bg_hue": "200",
+ "bg_sat": "70",
"nl_br": "23",
"active_mode": "0",
"current_brightness": "30",
@@ -74,9 +76,7 @@ YAML_CONFIGURATION = {
}
}
-CONFIG_ENTRY_DATA = {
- CONF_ID: ID,
-}
+CONFIG_ENTRY_DATA = {CONF_ID: ID}
def _mocked_bulb(cannot_connect=False):
diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py
index dd5ae85e89e..8994c8e3360 100644
--- a/tests/components/yeelight/test_config_flow.py
+++ b/tests/components/yeelight/test_config_flow.py
@@ -56,17 +56,13 @@ async def test_discovery(hass: HomeAssistant):
assert not result["errors"]
with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"):
- result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {},
- )
+ result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result2["type"] == "form"
assert result2["step_id"] == "pick_device"
assert not result2["errors"]
with patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, patch(
- f"{MODULE}.async_setup_entry",
- return_value=True,
+ f"{MODULE}.async_setup_entry", return_value=True
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_DEVICE: ID}
@@ -87,10 +83,7 @@ async def test_discovery(hass: HomeAssistant):
assert not result["errors"]
with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"):
- result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {},
- )
+ result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result2["type"] == "abort"
assert result2["reason"] == "no_devices_found"
@@ -102,10 +95,7 @@ async def test_discovery_no_device(hass: HomeAssistant):
)
with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight", no_device=True):
- result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {},
- )
+ result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result2["type"] == "abort"
assert result2["reason"] == "no_devices_found"
@@ -138,8 +128,7 @@ async def test_import(hass: HomeAssistant):
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch(
f"{MODULE}.async_setup", return_value=True
) as mock_setup, patch(
- f"{MODULE}.async_setup_entry",
- return_value=True,
+ f"{MODULE}.async_setup_entry", return_value=True
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
@@ -200,10 +189,7 @@ async def test_manual(hass: HomeAssistant):
mocked_bulb = _mocked_bulb()
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch(
f"{MODULE}.async_setup", return_value=True
- ), patch(
- f"{MODULE}.async_setup_entry",
- return_value=True,
- ):
+ ), patch(f"{MODULE}.async_setup_entry", return_value=True):
result4 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_HOST: IP_ADDRESS}
)
@@ -279,10 +265,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant):
type(mocked_bulb).get_capabilities = MagicMock(return_value=None)
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch(
f"{MODULE}.async_setup", return_value=True
- ), patch(
- f"{MODULE}.async_setup_entry",
- return_value=True,
- ):
+ ), patch(f"{MODULE}.async_setup_entry", return_value=True):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_HOST: IP_ADDRESS}
)
@@ -354,16 +337,13 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data):
mocked_bulb = _mocked_bulb()
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": source},
- data=data,
+ DOMAIN, context={"source": source}, data=data
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] is None
with patch(f"{MODULE}.async_setup", return_value=True) as mock_async_setup, patch(
- f"{MODULE}.async_setup_entry",
- return_value=True,
+ f"{MODULE}.async_setup_entry", return_value=True
) as mock_async_setup_entry:
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result2["type"] == "create_entry"
@@ -393,9 +373,7 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data
type(mocked_bulb).get_capabilities = MagicMock(return_value=None)
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": source},
- data=data,
+ DOMAIN, context={"source": source}, data=data
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "cannot_connect"
diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py
index 8a37c2b283e..2d1113d1896 100644
--- a/tests/components/yeelight/test_init.py
+++ b/tests/components/yeelight/test_init.py
@@ -45,12 +45,7 @@ from tests.common import MockConfigEntry
async def test_ip_changes_fallback_discovery(hass: HomeAssistant):
"""Test Yeelight ip changes and we fallback to discovery."""
config_entry = MockConfigEntry(
- domain=DOMAIN,
- data={
- CONF_ID: ID,
- CONF_HOST: "5.5.5.5",
- },
- unique_id=ID,
+ domain=DOMAIN, data={CONF_ID: ID, CONF_HOST: "5.5.5.5"}, unique_id=ID
)
config_entry.add_to_hass(hass)
@@ -60,12 +55,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant):
side_effect=[OSError, CAPABILITIES, CAPABILITIES]
)
- _discovered_devices = [
- {
- "capabilities": CAPABILITIES,
- "ip": IP_ADDRESS,
- }
- ]
+ _discovered_devices = [{"capabilities": CAPABILITIES, "ip": IP_ADDRESS}]
with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch(
f"{MODULE}.discover_bulbs", return_value=_discovered_devices
):
@@ -92,12 +82,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant):
async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant):
"""Test Yeelight ip changes and we fallback to discovery."""
- config_entry = MockConfigEntry(
- domain=DOMAIN,
- data={
- CONF_HOST: "5.5.5.5",
- },
- )
+ config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "5.5.5.5"})
config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb(True)
@@ -170,10 +155,7 @@ async def test_unique_ids_device(hass: HomeAssistant):
"""Test Yeelight unique IDs from yeelight device IDs."""
config_entry = MockConfigEntry(
domain=DOMAIN,
- data={
- **CONFIG_ENTRY_DATA,
- CONF_NIGHTLIGHT_SWITCH: True,
- },
+ data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True},
unique_id=ID,
)
config_entry.add_to_hass(hass)
@@ -197,11 +179,7 @@ async def test_unique_ids_device(hass: HomeAssistant):
async def test_unique_ids_entry(hass: HomeAssistant):
"""Test Yeelight unique IDs from entry IDs."""
config_entry = MockConfigEntry(
- domain=DOMAIN,
- data={
- **CONFIG_ENTRY_DATA,
- CONF_NIGHTLIGHT_SWITCH: True,
- },
+ domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True}
)
config_entry.add_to_hass(hass)
@@ -231,12 +209,7 @@ async def test_unique_ids_entry(hass: HomeAssistant):
async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant):
"""Test Yeelight off while adding to ha, for example on HA start."""
config_entry = MockConfigEntry(
- domain=DOMAIN,
- data={
- **CONFIG_ENTRY_DATA,
- CONF_HOST: IP_ADDRESS,
- },
- unique_id=ID,
+ domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_HOST: IP_ADDRESS}, unique_id=ID
)
config_entry.add_to_hass(hass)
diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py
index b6ce2fa4faf..9283514cb70 100644
--- a/tests/components/yeelight/test_light.py
+++ b/tests/components/yeelight/test_light.py
@@ -77,8 +77,6 @@ from homeassistant.components.yeelight.light import (
SERVICE_SET_MUSIC_MODE,
SERVICE_START_FLOW,
SUPPORT_YEELIGHT,
- SUPPORT_YEELIGHT_RGB,
- SUPPORT_YEELIGHT_WHITE_TEMP,
YEELIGHT_COLOR_EFFECT_LIST,
YEELIGHT_MONO_EFFECT_LIST,
YEELIGHT_TEMP_ONLY_EFFECT_LIST,
@@ -171,7 +169,91 @@ async def test_services(hass: HomeAssistant, caplog):
== err_count + 1
)
- # turn_on
+ # turn_on rgb_color
+ brightness = 100
+ rgb_color = (0, 128, 255)
+ transition = 2
+ await hass.services.async_call(
+ "light",
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: ENTITY_LIGHT,
+ ATTR_BRIGHTNESS: brightness,
+ ATTR_RGB_COLOR: rgb_color,
+ ATTR_FLASH: FLASH_LONG,
+ ATTR_EFFECT: EFFECT_STOP,
+ ATTR_TRANSITION: transition,
+ },
+ blocking=True,
+ )
+ mocked_bulb.turn_on.assert_called_once_with(
+ duration=transition * 1000,
+ light_type=LightType.Main,
+ power_mode=PowerMode.NORMAL,
+ )
+ mocked_bulb.turn_on.reset_mock()
+ mocked_bulb.start_music.assert_called_once()
+ mocked_bulb.start_music.reset_mock()
+ mocked_bulb.set_brightness.assert_called_once_with(
+ brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main
+ )
+ mocked_bulb.set_brightness.reset_mock()
+ mocked_bulb.set_color_temp.assert_not_called()
+ mocked_bulb.set_color_temp.reset_mock()
+ mocked_bulb.set_hsv.assert_not_called()
+ mocked_bulb.set_hsv.reset_mock()
+ mocked_bulb.set_rgb.assert_called_once_with(
+ *rgb_color, duration=transition * 1000, light_type=LightType.Main
+ )
+ mocked_bulb.set_rgb.reset_mock()
+ mocked_bulb.start_flow.assert_called_once() # flash
+ mocked_bulb.start_flow.reset_mock()
+ mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main)
+ mocked_bulb.stop_flow.reset_mock()
+
+ # turn_on hs_color
+ brightness = 100
+ hs_color = (180, 100)
+ transition = 2
+ await hass.services.async_call(
+ "light",
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: ENTITY_LIGHT,
+ ATTR_BRIGHTNESS: brightness,
+ ATTR_HS_COLOR: hs_color,
+ ATTR_FLASH: FLASH_LONG,
+ ATTR_EFFECT: EFFECT_STOP,
+ ATTR_TRANSITION: transition,
+ },
+ blocking=True,
+ )
+ mocked_bulb.turn_on.assert_called_once_with(
+ duration=transition * 1000,
+ light_type=LightType.Main,
+ power_mode=PowerMode.NORMAL,
+ )
+ mocked_bulb.turn_on.reset_mock()
+ mocked_bulb.start_music.assert_called_once()
+ mocked_bulb.start_music.reset_mock()
+ mocked_bulb.set_brightness.assert_called_once_with(
+ brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main
+ )
+ mocked_bulb.set_brightness.reset_mock()
+ mocked_bulb.set_color_temp.assert_not_called()
+ mocked_bulb.set_color_temp.reset_mock()
+ mocked_bulb.set_hsv.assert_called_once_with(
+ *hs_color, duration=transition * 1000, light_type=LightType.Main
+ )
+ mocked_bulb.set_hsv.reset_mock()
+ mocked_bulb.set_rgb.assert_not_called()
+ mocked_bulb.set_rgb.reset_mock()
+ mocked_bulb.start_flow.assert_called_once() # flash
+ mocked_bulb.start_flow.reset_mock()
+ mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main)
+ mocked_bulb.stop_flow.reset_mock()
+
+ # turn_on color_temp
brightness = 100
color_temp = 200
transition = 1
@@ -203,6 +285,8 @@ async def test_services(hass: HomeAssistant, caplog):
duration=transition * 1000,
light_type=LightType.Main,
)
+ mocked_bulb.set_hsv.assert_not_called()
+ mocked_bulb.set_rgb.assert_not_called()
mocked_bulb.start_flow.assert_called_once() # flash
mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main)
@@ -331,12 +415,12 @@ async def test_services(hass: HomeAssistant, caplog):
)
-async def test_device_types(hass: HomeAssistant):
+async def test_device_types(hass: HomeAssistant, caplog):
"""Test different device types."""
mocked_bulb = _mocked_bulb()
properties = {**PROPERTIES}
properties.pop("active_mode")
- properties["color_mode"] = "3"
+ properties["color_mode"] = "3" # HSV
mocked_bulb.last_properties = properties
async def _async_setup(config_entry):
@@ -353,11 +437,7 @@ async def test_device_types(hass: HomeAssistant):
entity_id=ENTITY_LIGHT,
):
config_entry = MockConfigEntry(
- domain=DOMAIN,
- data={
- **CONFIG_ENTRY_DATA,
- CONF_NIGHTLIGHT_SWITCH: False,
- },
+ domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}
)
config_entry.add_to_hass(hass)
@@ -383,11 +463,7 @@ async def test_device_types(hass: HomeAssistant):
if nightlight_properties is None:
return
config_entry = MockConfigEntry(
- domain=DOMAIN,
- data={
- **CONFIG_ENTRY_DATA,
- CONF_NIGHTLIGHT_SWITCH: True,
- },
+ domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True}
)
config_entry.add_to_hass(hass)
await _async_setup(config_entry)
@@ -411,15 +487,16 @@ async def test_device_types(hass: HomeAssistant):
ct = color_temperature_kelvin_to_mired(int(PROPERTIES["ct"]))
hue = int(PROPERTIES["hue"])
sat = int(PROPERTIES["sat"])
- hs_color = (round(hue / 360 * 65536, 3), round(sat / 100 * 255, 3))
- rgb_color = color_hs_to_RGB(*hs_color)
- xy_color = color_hs_to_xy(*hs_color)
+ rgb = int(PROPERTIES["rgb"])
+ rgb_color = ((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF)
+ hs_color = (hue, sat)
bg_bright = round(255 * int(PROPERTIES["bg_bright"]) / 100)
bg_ct = color_temperature_kelvin_to_mired(int(PROPERTIES["bg_ct"]))
+ bg_hue = int(PROPERTIES["bg_hue"])
+ bg_sat = int(PROPERTIES["bg_sat"])
bg_rgb = int(PROPERTIES["bg_rgb"])
+ bg_hs_color = (bg_hue, bg_sat)
bg_rgb_color = ((bg_rgb >> 16) & 0xFF, (bg_rgb >> 8) & 0xFF, bg_rgb & 0xFF)
- bg_hs_color = color_RGB_to_hs(*bg_rgb_color)
- bg_xy_color = color_RGB_to_xy(*bg_rgb_color)
nl_br = round(255 * int(PROPERTIES["nl_br"]) / 100)
# Default
@@ -448,14 +525,15 @@ async def test_device_types(hass: HomeAssistant):
},
)
- # Color
+ # Color - color mode CT
+ mocked_bulb.last_properties["color_mode"] = "2" # CT
model_specs = _MODEL_SPECS["color"]
await _async_test(
BulbType.Color,
"color",
{
"effect_list": YEELIGHT_COLOR_EFFECT_LIST,
- "supported_features": SUPPORT_YEELIGHT_RGB,
+ "supported_features": SUPPORT_YEELIGHT,
"min_mireds": color_temperature_kelvin_to_mired(
model_specs["color_temp"]["max"]
),
@@ -464,11 +542,8 @@ async def test_device_types(hass: HomeAssistant):
),
"brightness": current_brightness,
"color_temp": ct,
- "hs_color": hs_color,
- "rgb_color": rgb_color,
- "xy_color": xy_color,
- "color_mode": "hs",
- "supported_color_modes": ["color_temp", "hs"],
+ "color_mode": "color_temp",
+ "supported_color_modes": ["color_temp", "hs", "rgb"],
},
{
"supported_features": 0,
@@ -477,6 +552,144 @@ async def test_device_types(hass: HomeAssistant):
},
)
+ # Color - color mode HS
+ mocked_bulb.last_properties["color_mode"] = "3" # HSV
+ model_specs = _MODEL_SPECS["color"]
+ await _async_test(
+ BulbType.Color,
+ "color",
+ {
+ "effect_list": YEELIGHT_COLOR_EFFECT_LIST,
+ "supported_features": SUPPORT_YEELIGHT,
+ "min_mireds": color_temperature_kelvin_to_mired(
+ model_specs["color_temp"]["max"]
+ ),
+ "max_mireds": color_temperature_kelvin_to_mired(
+ model_specs["color_temp"]["min"]
+ ),
+ "brightness": current_brightness,
+ "hs_color": hs_color,
+ "rgb_color": color_hs_to_RGB(*hs_color),
+ "xy_color": color_hs_to_xy(*hs_color),
+ "color_mode": "hs",
+ "supported_color_modes": ["color_temp", "hs", "rgb"],
+ },
+ {
+ "supported_features": 0,
+ "color_mode": "onoff",
+ "supported_color_modes": ["onoff"],
+ },
+ )
+
+ # Color - color mode RGB
+ mocked_bulb.last_properties["color_mode"] = "1" # RGB
+ model_specs = _MODEL_SPECS["color"]
+ await _async_test(
+ BulbType.Color,
+ "color",
+ {
+ "effect_list": YEELIGHT_COLOR_EFFECT_LIST,
+ "supported_features": SUPPORT_YEELIGHT,
+ "min_mireds": color_temperature_kelvin_to_mired(
+ model_specs["color_temp"]["max"]
+ ),
+ "max_mireds": color_temperature_kelvin_to_mired(
+ model_specs["color_temp"]["min"]
+ ),
+ "brightness": current_brightness,
+ "hs_color": color_RGB_to_hs(*rgb_color),
+ "rgb_color": rgb_color,
+ "xy_color": color_RGB_to_xy(*rgb_color),
+ "color_mode": "rgb",
+ "supported_color_modes": ["color_temp", "hs", "rgb"],
+ },
+ {
+ "supported_features": 0,
+ "color_mode": "onoff",
+ "supported_color_modes": ["onoff"],
+ },
+ )
+
+ # Color - color mode HS but no hue
+ mocked_bulb.last_properties["color_mode"] = "3" # HSV
+ mocked_bulb.last_properties["hue"] = None
+ model_specs = _MODEL_SPECS["color"]
+ await _async_test(
+ BulbType.Color,
+ "color",
+ {
+ "effect_list": YEELIGHT_COLOR_EFFECT_LIST,
+ "supported_features": SUPPORT_YEELIGHT,
+ "min_mireds": color_temperature_kelvin_to_mired(
+ model_specs["color_temp"]["max"]
+ ),
+ "max_mireds": color_temperature_kelvin_to_mired(
+ model_specs["color_temp"]["min"]
+ ),
+ "brightness": current_brightness,
+ "color_mode": "hs",
+ "supported_color_modes": ["color_temp", "hs", "rgb"],
+ },
+ {
+ "supported_features": 0,
+ "color_mode": "onoff",
+ "supported_color_modes": ["onoff"],
+ },
+ )
+
+ # Color - color mode RGB but no color
+ mocked_bulb.last_properties["color_mode"] = "1" # RGB
+ mocked_bulb.last_properties["rgb"] = None
+ model_specs = _MODEL_SPECS["color"]
+ await _async_test(
+ BulbType.Color,
+ "color",
+ {
+ "effect_list": YEELIGHT_COLOR_EFFECT_LIST,
+ "supported_features": SUPPORT_YEELIGHT,
+ "min_mireds": color_temperature_kelvin_to_mired(
+ model_specs["color_temp"]["max"]
+ ),
+ "max_mireds": color_temperature_kelvin_to_mired(
+ model_specs["color_temp"]["min"]
+ ),
+ "brightness": current_brightness,
+ "color_mode": "rgb",
+ "supported_color_modes": ["color_temp", "hs", "rgb"],
+ },
+ {
+ "supported_features": 0,
+ "color_mode": "onoff",
+ "supported_color_modes": ["onoff"],
+ },
+ )
+
+ # Color - unsupported color_mode
+ mocked_bulb.last_properties["color_mode"] = 4 # Unsupported
+ model_specs = _MODEL_SPECS["color"]
+ await _async_test(
+ BulbType.Color,
+ "color",
+ {
+ "effect_list": YEELIGHT_COLOR_EFFECT_LIST,
+ "supported_features": SUPPORT_YEELIGHT,
+ "min_mireds": color_temperature_kelvin_to_mired(
+ model_specs["color_temp"]["max"]
+ ),
+ "max_mireds": color_temperature_kelvin_to_mired(
+ model_specs["color_temp"]["min"]
+ ),
+ "color_mode": "unknown",
+ "supported_color_modes": ["color_temp", "hs", "rgb"],
+ },
+ {
+ "supported_features": 0,
+ "color_mode": "onoff",
+ "supported_color_modes": ["onoff"],
+ },
+ )
+ assert "Light reported unknown color mode: 4" in caplog.text
+
# WhiteTemp
model_specs = _MODEL_SPECS["ceiling1"]
await _async_test(
@@ -484,7 +697,7 @@ async def test_device_types(hass: HomeAssistant):
"ceiling1",
{
"effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST,
- "supported_features": SUPPORT_YEELIGHT_WHITE_TEMP,
+ "supported_features": SUPPORT_YEELIGHT,
"min_mireds": color_temperature_kelvin_to_mired(
model_specs["color_temp"]["max"]
),
@@ -517,7 +730,7 @@ async def test_device_types(hass: HomeAssistant):
"effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST,
"flowing": False,
"night_light": True,
- "supported_features": SUPPORT_YEELIGHT_WHITE_TEMP,
+ "supported_features": SUPPORT_YEELIGHT,
"min_mireds": color_temperature_kelvin_to_mired(
model_specs["color_temp"]["max"]
),
@@ -537,21 +750,62 @@ async def test_device_types(hass: HomeAssistant):
"supported_color_modes": ["brightness"],
},
)
+ # Background light - color mode CT
+ mocked_bulb.last_properties["bg_lmode"] = "2" # CT
await _async_test(
BulbType.WhiteTempMood,
"ceiling4",
{
"effect_list": YEELIGHT_COLOR_EFFECT_LIST,
- "supported_features": SUPPORT_YEELIGHT_RGB,
+ "supported_features": SUPPORT_YEELIGHT,
"min_mireds": color_temperature_kelvin_to_mired(6500),
"max_mireds": color_temperature_kelvin_to_mired(1700),
"brightness": bg_bright,
"color_temp": bg_ct,
+ "color_mode": "color_temp",
+ "supported_color_modes": ["color_temp", "hs", "rgb"],
+ },
+ name=f"{UNIQUE_NAME} ambilight",
+ entity_id=f"{ENTITY_LIGHT}_ambilight",
+ )
+
+ # Background light - color mode HS
+ mocked_bulb.last_properties["bg_lmode"] = "3" # HS
+ await _async_test(
+ BulbType.WhiteTempMood,
+ "ceiling4",
+ {
+ "effect_list": YEELIGHT_COLOR_EFFECT_LIST,
+ "supported_features": SUPPORT_YEELIGHT,
+ "min_mireds": color_temperature_kelvin_to_mired(6500),
+ "max_mireds": color_temperature_kelvin_to_mired(1700),
+ "brightness": bg_bright,
"hs_color": bg_hs_color,
- "rgb_color": bg_rgb_color,
- "xy_color": bg_xy_color,
+ "rgb_color": color_hs_to_RGB(*bg_hs_color),
+ "xy_color": color_hs_to_xy(*bg_hs_color),
"color_mode": "hs",
- "supported_color_modes": ["color_temp", "hs"],
+ "supported_color_modes": ["color_temp", "hs", "rgb"],
+ },
+ name=f"{UNIQUE_NAME} ambilight",
+ entity_id=f"{ENTITY_LIGHT}_ambilight",
+ )
+
+ # Background light - color mode RGB
+ mocked_bulb.last_properties["bg_lmode"] = "1" # RGB
+ await _async_test(
+ BulbType.WhiteTempMood,
+ "ceiling4",
+ {
+ "effect_list": YEELIGHT_COLOR_EFFECT_LIST,
+ "supported_features": SUPPORT_YEELIGHT,
+ "min_mireds": color_temperature_kelvin_to_mired(6500),
+ "max_mireds": color_temperature_kelvin_to_mired(1700),
+ "brightness": bg_bright,
+ "hs_color": color_RGB_to_hs(*bg_rgb_color),
+ "rgb_color": bg_rgb_color,
+ "xy_color": color_RGB_to_xy(*bg_rgb_color),
+ "color_mode": "rgb",
+ "supported_color_modes": ["color_temp", "hs", "rgb"],
},
name=f"{UNIQUE_NAME} ambilight",
entity_id=f"{ENTITY_LIGHT}_ambilight",
@@ -577,16 +831,13 @@ async def test_effects(hass: HomeAssistant):
{YEELIGHT_SLEEP_TRANSACTION: [800]},
],
},
- },
- ],
- },
+ }
+ ]
+ }
},
)
- config_entry = MockConfigEntry(
- domain=DOMAIN,
- data=CONFIG_ENTRY_DATA,
- )
+ config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA)
config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb()
diff --git a/tests/components/zeroconf/conftest.py b/tests/components/zeroconf/conftest.py
new file mode 100644
index 00000000000..5ccd617f84f
--- /dev/null
+++ b/tests/components/zeroconf/conftest.py
@@ -0,0 +1,15 @@
+"""Tests for the Zeroconf component."""
+from unittest.mock import AsyncMock, patch
+
+import pytest
+
+
+@pytest.fixture
+def mock_async_zeroconf():
+ """Mock AsyncZeroconf."""
+ with patch("homeassistant.components.zeroconf.HaAsyncZeroconf") as mock_aiozc:
+ zc = mock_aiozc.return_value
+ zc.async_register_service = AsyncMock()
+ zc.zeroconf.async_wait_for_start = AsyncMock()
+ zc.ha_async_close = AsyncMock()
+ yield zc
diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py
index ef0ab1fda60..68c0785e60b 100644
--- a/tests/components/zeroconf/test_init.py
+++ b/tests/components/zeroconf/test_init.py
@@ -1,7 +1,8 @@
"""Test Zeroconf component setup process."""
from unittest.mock import call, patch
-from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange
+from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange
+from zeroconf.asyncio import AsyncServiceInfo
from homeassistant.components import zeroconf
from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6
@@ -24,29 +25,8 @@ PROPERTIES = {
HOMEKIT_STATUS_UNPAIRED = b"1"
HOMEKIT_STATUS_PAIRED = b"0"
-_ROUTE_NO_LOOPBACK = (
- {
- "attrs": [
- ("RTA_TABLE", 254),
- ("RTA_DST", "224.0.0.251"),
- ("RTA_OIF", 4),
- ("RTA_PREFSRC", "192.168.1.5"),
- ],
- },
-)
-_ROUTE_LOOPBACK = (
- {
- "attrs": [
- ("RTA_TABLE", 254),
- ("RTA_DST", "224.0.0.251"),
- ("RTA_OIF", 4),
- ("RTA_PREFSRC", "127.0.0.1"),
- ],
- },
-)
-
-def service_update_mock(zeroconf, services, handlers, *, limit_service=None):
+def service_update_mock(ipv6, zeroconf, services, handlers, *, limit_service=None):
"""Call service update handler."""
for service in services:
if limit_service is not None and service != limit_service:
@@ -56,7 +36,7 @@ def service_update_mock(zeroconf, services, handlers, *, limit_service=None):
def get_service_info_mock(service_type, name):
"""Return service info for get_service_info."""
- return ServiceInfo(
+ return AsyncServiceInfo(
service_type,
name,
addresses=[b"\n\x00\x00\x14"],
@@ -70,7 +50,7 @@ def get_service_info_mock(service_type, name):
def get_service_info_mock_without_an_address(service_type, name):
"""Return service info for get_service_info without any addresses."""
- return ServiceInfo(
+ return AsyncServiceInfo(
service_type,
name,
addresses=[],
@@ -86,7 +66,7 @@ def get_homekit_info_mock(model, pairing_status):
"""Return homekit info for get_service_info for an homekit device."""
def mock_homekit_info(service_type, name):
- return ServiceInfo(
+ return AsyncServiceInfo(
service_type,
name,
addresses=[b"\n\x00\x00\x14"],
@@ -104,7 +84,7 @@ def get_zeroconf_info_mock(macaddress):
"""Return info for get_service_info for an zeroconf device."""
def mock_zc_info(service_type, name):
- return ServiceInfo(
+ return AsyncServiceInfo(
service_type,
name,
addresses=[b"\n\x00\x00\x14"],
@@ -122,7 +102,7 @@ def get_zeroconf_info_mock_manufacturer(manufacturer):
"""Return info for get_service_info for an zeroconf device."""
def mock_zc_info(service_type, name):
- return ServiceInfo(
+ return AsyncServiceInfo(
service_type,
name,
addresses=[b"\n\x00\x00\x14"],
@@ -136,14 +116,14 @@ def get_zeroconf_info_mock_manufacturer(manufacturer):
return mock_zc_info
-async def test_setup(hass, mock_zeroconf):
+async def test_setup(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
- zeroconf, "HaServiceBrowser", side_effect=service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -162,13 +142,15 @@ async def test_setup(hass, mock_zeroconf):
# Test instance is set.
assert "zeroconf" in hass.data
- assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf
+ assert (
+ await hass.components.zeroconf.async_get_async_instance() is mock_async_zeroconf
+ )
-async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog):
+async def test_setup_with_overly_long_url_and_name(hass, mock_async_zeroconf, caplog):
"""Test we still setup with long urls and names."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
- zeroconf, "HaServiceBrowser", side_effect=service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.get_url",
return_value="https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a/bit/longer/than/the/maximum/length/that/we/allow/for/a/value",
@@ -177,7 +159,7 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog):
"location_name",
"\u00dcBER \u00dcber German Umlaut long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string",
), patch(
- "homeassistant.components.zeroconf.ServiceInfo.request",
+ "homeassistant.components.zeroconf.AsyncServiceInfo.request",
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
@@ -187,12 +169,12 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog):
assert "German Umlaut" in caplog.text
-async def test_setup_with_default_interface(hass, mock_zeroconf):
+async def test_setup_with_default_interface(hass, mock_async_zeroconf):
"""Test default interface config."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
- zeroconf, "HaServiceBrowser", side_effect=service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(
@@ -201,30 +183,30 @@ async def test_setup_with_default_interface(hass, mock_zeroconf):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
- assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.Default)
+ assert mock_async_zeroconf.called_with(interface_choice=InterfaceChoice.Default)
-async def test_setup_without_default_interface(hass, mock_zeroconf):
+async def test_setup_without_default_interface(hass, mock_async_zeroconf):
"""Test without default interface config."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
- zeroconf, "HaServiceBrowser", side_effect=service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(
hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: False}}
)
- assert mock_zeroconf.called_with()
+ assert mock_async_zeroconf.called_with()
-async def test_setup_without_ipv6(hass, mock_zeroconf):
+async def test_setup_without_ipv6(hass, mock_async_zeroconf):
"""Test without ipv6."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
- zeroconf, "HaServiceBrowser", side_effect=service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(
@@ -233,15 +215,15 @@ async def test_setup_without_ipv6(hass, mock_zeroconf):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
- assert mock_zeroconf.called_with(ip_version=IPVersion.V4Only)
+ assert mock_async_zeroconf.called_with(ip_version=IPVersion.V4Only)
-async def test_setup_with_ipv6(hass, mock_zeroconf):
+async def test_setup_with_ipv6(hass, mock_async_zeroconf):
"""Test without ipv6."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
- zeroconf, "HaServiceBrowser", side_effect=service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(
@@ -250,28 +232,28 @@ async def test_setup_with_ipv6(hass, mock_zeroconf):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
- assert mock_zeroconf.called_with()
+ assert mock_async_zeroconf.called_with()
-async def test_setup_with_ipv6_default(hass, mock_zeroconf):
+async def test_setup_with_ipv6_default(hass, mock_async_zeroconf):
"""Test without ipv6 as default."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
- zeroconf, "HaServiceBrowser", side_effect=service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
- assert mock_zeroconf.called_with()
+ assert mock_async_zeroconf.called_with()
-async def test_zeroconf_match_macaddress(hass, mock_zeroconf):
+async def test_zeroconf_match_macaddress(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
- def http_only_service_update_mock(zeroconf, services, handlers):
+ def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
@@ -291,9 +273,9 @@ async def test_zeroconf_match_macaddress(hass, mock_zeroconf):
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
- zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -305,10 +287,10 @@ async def test_zeroconf_match_macaddress(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "shelly"
-async def test_zeroconf_match_manufacturer(hass, mock_zeroconf):
+async def test_zeroconf_match_manufacturer(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
- def http_only_service_update_mock(zeroconf, services, handlers):
+ def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
@@ -324,9 +306,9 @@ async def test_zeroconf_match_manufacturer(hass, mock_zeroconf):
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
- zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_zeroconf_info_mock_manufacturer("Samsung Electronics"),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -338,10 +320,10 @@ async def test_zeroconf_match_manufacturer(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "samsungtv"
-async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf):
+async def test_zeroconf_match_manufacturer_not_present(hass, mock_async_zeroconf):
"""Test matchers reject when a property is missing."""
- def http_only_service_update_mock(zeroconf, services, handlers):
+ def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
@@ -357,9 +339,9 @@ async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf):
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
- zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_zeroconf_info_mock("aabbccddeeff"),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -370,10 +352,10 @@ async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf):
assert len(mock_config_flow.mock_calls) == 0
-async def test_zeroconf_no_match(hass, mock_zeroconf):
+async def test_zeroconf_no_match(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
- def http_only_service_update_mock(zeroconf, services, handlers):
+ def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
@@ -389,9 +371,9 @@ async def test_zeroconf_no_match(hass, mock_zeroconf):
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
- zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -402,10 +384,10 @@ async def test_zeroconf_no_match(hass, mock_zeroconf):
assert len(mock_config_flow.mock_calls) == 0
-async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf):
+async def test_zeroconf_no_match_manufacturer(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
- def http_only_service_update_mock(zeroconf, services, handlers):
+ def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
@@ -421,9 +403,9 @@ async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf):
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
- zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_zeroconf_info_mock_manufacturer("Not Samsung Electronics"),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -434,7 +416,7 @@ async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf):
assert len(mock_config_flow.mock_calls) == 0
-async def test_homekit_match_partial_space(hass, mock_zeroconf):
+async def test_homekit_match_partial_space(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(
zc_gen.ZEROCONF,
@@ -444,12 +426,12 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf):
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf,
- "HaServiceBrowser",
+ "HaAsyncServiceBrowser",
side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._tcp.local."
),
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock("LIFX bulb", HOMEKIT_STATUS_UNPAIRED),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -461,7 +443,7 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "lifx"
-async def test_homekit_match_partial_dash(hass, mock_zeroconf):
+async def test_homekit_match_partial_dash(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(
zc_gen.ZEROCONF,
@@ -471,12 +453,12 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf):
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf,
- "HaServiceBrowser",
+ "HaAsyncServiceBrowser",
side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._udp.local."
),
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock("Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -488,7 +470,7 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "rachio"
-async def test_homekit_match_partial_fnmatch(hass, mock_zeroconf):
+async def test_homekit_match_partial_fnmatch(hass, mock_async_zeroconf):
"""Test matching homekit devices with fnmatch."""
with patch.dict(
zc_gen.ZEROCONF,
@@ -498,12 +480,12 @@ async def test_homekit_match_partial_fnmatch(hass, mock_zeroconf):
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf,
- "HaServiceBrowser",
+ "HaAsyncServiceBrowser",
side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._tcp.local."
),
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock("YLDP13YL", HOMEKIT_STATUS_UNPAIRED),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -515,7 +497,7 @@ async def test_homekit_match_partial_fnmatch(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "yeelight"
-async def test_homekit_match_full(hass, mock_zeroconf):
+async def test_homekit_match_full(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(
zc_gen.ZEROCONF,
@@ -525,12 +507,12 @@ async def test_homekit_match_full(hass, mock_zeroconf):
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf,
- "HaServiceBrowser",
+ "HaAsyncServiceBrowser",
side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._udp.local."
),
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -542,7 +524,7 @@ async def test_homekit_match_full(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "hue"
-async def test_homekit_already_paired(hass, mock_zeroconf):
+async def test_homekit_already_paired(hass, mock_async_zeroconf):
"""Test that an already paired device is sent to homekit_controller."""
with patch.dict(
zc_gen.ZEROCONF,
@@ -552,12 +534,12 @@ async def test_homekit_already_paired(hass, mock_zeroconf):
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf,
- "HaServiceBrowser",
+ "HaAsyncServiceBrowser",
side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._tcp.local."
),
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock("tado", HOMEKIT_STATUS_PAIRED),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -570,7 +552,7 @@ async def test_homekit_already_paired(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller"
-async def test_homekit_invalid_paring_status(hass, mock_zeroconf):
+async def test_homekit_invalid_paring_status(hass, mock_async_zeroconf):
"""Test that missing paring data is not sent to homekit_controller."""
with patch.dict(
zc_gen.ZEROCONF,
@@ -580,12 +562,12 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf):
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf,
- "HaServiceBrowser",
+ "HaAsyncServiceBrowser",
side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._tcp.local."
),
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock("tado", b"invalid"),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -597,7 +579,7 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "tado"
-async def test_homekit_not_paired(hass, mock_zeroconf):
+async def test_homekit_not_paired(hass, mock_async_zeroconf):
"""Test that an not paired device is sent to homekit_controller."""
with patch.dict(
zc_gen.ZEROCONF,
@@ -606,9 +588,9 @@ async def test_homekit_not_paired(hass, mock_zeroconf):
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
- zeroconf, "HaServiceBrowser", side_effect=service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
) as mock_service_browser, patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock(
"this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED
),
@@ -646,19 +628,21 @@ async def test_info_from_service_with_addresses(hass):
assert info is None
-async def test_get_instance(hass, mock_zeroconf):
+async def test_get_instance(hass, mock_async_zeroconf):
"""Test we get an instance."""
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
- assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf
+ assert (
+ await hass.components.zeroconf.async_get_async_instance() is mock_async_zeroconf
+ )
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
- assert len(mock_zeroconf.ha_close.mock_calls) == 1
+ assert len(mock_async_zeroconf.ha_async_close.mock_calls) == 1
-async def test_removed_ignored(hass, mock_zeroconf):
+async def test_removed_ignored(hass, mock_async_zeroconf):
"""Test we remove it when a zeroconf entry is removed."""
- def service_update_mock(zeroconf, services, handlers):
+ def service_update_mock(ipv6, zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
@@ -680,9 +664,9 @@ async def test_removed_ignored(hass, mock_zeroconf):
)
with patch.object(
- zeroconf, "HaServiceBrowser", side_effect=service_update_mock
+ zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
) as mock_service_info:
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@@ -709,24 +693,28 @@ _ADAPTER_WITH_DEFAULT_ENABLED = [
]
-async def test_async_detect_interfaces_setting_non_loopback_route(hass):
+async def test_async_detect_interfaces_setting_non_loopback_route(
+ hass, mock_async_zeroconf
+):
"""Test without default interface config and the route returns a non-loopback address."""
- with patch(
- "homeassistant.components.zeroconf.models.HaZeroconf"
- ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
- zeroconf, "HaServiceBrowser", side_effect=service_update_mock
+ with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object(
+ hass.config_entries.flow, "async_init"
+ ), patch.object(
+ zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.network.async_get_adapters",
return_value=_ADAPTER_WITH_DEFAULT_ENABLED,
), patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
- assert mock_zc.mock_calls[0] == call(interfaces=InterfaceChoice.Default)
+ assert mock_zc.mock_calls[0] == call(
+ interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only
+ )
_ADAPTERS_WITH_MANUAL_CONFIG = [
@@ -764,17 +752,17 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [
]
-async def test_async_detect_interfaces_setting_empty_route(hass):
+async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zeroconf):
"""Test without default interface config and the route returns nothing."""
- with patch(
- "homeassistant.components.zeroconf.models.HaZeroconf"
- ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
- zeroconf, "HaServiceBrowser", side_effect=service_update_mock
+ with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object(
+ hass.config_entries.flow, "async_init"
+ ), patch.object(
+ zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.network.async_get_adapters",
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
), patch(
- "homeassistant.components.zeroconf.ServiceInfo",
+ "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py
index 0f902632db8..40c813b506c 100644
--- a/tests/components/zeroconf/test_usage.py
+++ b/tests/components/zeroconf/test_usage.py
@@ -10,7 +10,9 @@ from homeassistant.setup import async_setup_component
DOMAIN = "zeroconf"
-async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog):
+async def test_multiple_zeroconf_instances(
+ hass, mock_async_zeroconf, mock_zeroconf, caplog
+):
"""Test creating multiple zeroconf throws without an integration."""
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@@ -24,7 +26,9 @@ async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog):
assert "Zeroconf" in caplog.text
-async def test_multiple_zeroconf_instances_gives_shared(hass, mock_zeroconf, caplog):
+async def test_multiple_zeroconf_instances_gives_shared(
+ hass, mock_async_zeroconf, mock_zeroconf, caplog
+):
"""Test creating multiple zeroconf gives the shared instance to an integration."""
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py
index 45caed95ae6..eb65cc4fd2e 100644
--- a/tests/components/zha/common.py
+++ b/tests/components/zha/common.py
@@ -40,8 +40,9 @@ class FakeEndpoint:
if _patch_cluster:
patch_cluster(cluster)
self.in_clusters[cluster_id] = cluster
- if hasattr(cluster, "ep_attribute"):
- setattr(self, cluster.ep_attribute, cluster)
+ ep_attribute = cluster.ep_attribute
+ if ep_attribute:
+ setattr(self, ep_attribute, cluster)
def add_output_cluster(self, cluster_id, _patch_cluster=True):
"""Add an output cluster."""
diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py
index d3ab3c3ada2..0f696f21572 100644
--- a/tests/components/zha/test_device.py
+++ b/tests/components/zha/test_device.py
@@ -8,7 +8,10 @@ import pytest
import zigpy.profiles.zha
import zigpy.zcl.clusters.general as general
-import homeassistant.components.zha.core.device as zha_core_device
+from homeassistant.components.zha.core.const import (
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY,
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS,
+)
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE
import homeassistant.helpers.device_registry as dr
import homeassistant.util.dt as dt_util
@@ -117,13 +120,13 @@ async def test_check_available_success(
basic_ch.read_attributes.reset_mock()
device_with_basic_channel.last_seen = None
assert zha_device.available is True
- _send_time_changed(hass, zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2)
+ _send_time_changed(hass, zha_device.consider_unavailable_time + 2)
await hass.async_block_till_done()
assert zha_device.available is False
assert basic_ch.read_attributes.await_count == 0
device_with_basic_channel.last_seen = (
- time.time() - zha_core_device.CONSIDER_UNAVAILABLE_MAINS - 2
+ time.time() - zha_device.consider_unavailable_time - 2
)
_seens = [time.time(), device_with_basic_channel.last_seen]
@@ -172,7 +175,7 @@ async def test_check_available_unsuccessful(
assert basic_ch.read_attributes.await_count == 0
device_with_basic_channel.last_seen = (
- time.time() - zha_core_device.CONSIDER_UNAVAILABLE_MAINS - 2
+ time.time() - zha_device.consider_unavailable_time - 2
)
# unsuccessfuly ping zigpy device, but zha_device is still available
@@ -189,7 +192,7 @@ async def test_check_available_unsuccessful(
assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"]
assert zha_device.available is True
- # not even trying to update, device is unavailble
+ # not even trying to update, device is unavailable
_send_time_changed(hass, 91)
await hass.async_block_till_done()
assert basic_ch.read_attributes.await_count == 2
@@ -213,7 +216,7 @@ async def test_check_available_no_basic_channel(
assert zha_device.available is True
device_without_basic_channel.last_seen = (
- time.time() - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2
+ time.time() - zha_device.consider_unavailable_time - 2
)
assert "does not have a mandatory basic cluster" not in caplog.text
@@ -246,38 +249,38 @@ async def test_ota_sw_version(hass, ota_zha_device):
("zigpy_device", 0, True),
(
"zigpy_device",
- zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2,
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS + 2,
True,
),
(
"zigpy_device",
- zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2,
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY - 2,
True,
),
(
"zigpy_device",
- zha_core_device.CONSIDER_UNAVAILABLE_BATTERY + 2,
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY + 2,
False,
),
("zigpy_device_mains", 0, True),
(
"zigpy_device_mains",
- zha_core_device.CONSIDER_UNAVAILABLE_MAINS - 2,
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS - 2,
True,
),
(
"zigpy_device_mains",
- zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2,
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS + 2,
False,
),
(
"zigpy_device_mains",
- zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2,
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY - 2,
False,
),
(
"zigpy_device_mains",
- zha_core_device.CONSIDER_UNAVAILABLE_BATTERY + 2,
+ CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY + 2,
False,
),
),
diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py
index b2f964e2695..841d6b43400 100644
--- a/tests/components/zha/test_device_trigger.py
+++ b/tests/components/zha/test_device_trigger.py
@@ -7,7 +7,6 @@ import zigpy.profiles.zha
import zigpy.zcl.clusters.general as general
import homeassistant.components.automation as automation
-import homeassistant.components.zha.core.device as zha_core_device
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -252,9 +251,7 @@ async def test_device_offline_fires(
await hass.async_block_till_done()
assert zha_device.available is True
- zigpy_device.last_seen = (
- time.time() - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2
- )
+ zigpy_device.last_seen = time.time() - zha_device.consider_unavailable_time - 2
# there are 3 checkins to perform before marking the device unavailable
future = dt_util.utcnow() + timedelta(seconds=90)
@@ -266,7 +263,7 @@ async def test_device_offline_fires(
await hass.async_block_till_done()
future = dt_util.utcnow() + timedelta(
- seconds=zha_core_device.CONSIDER_UNAVAILABLE_BATTERY + 100
+ seconds=zha_device.consider_unavailable_time + 100
)
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py
index 5d7f66bacd5..4b3a9bec50c 100644
--- a/tests/components/zha/test_gateway.py
+++ b/tests/components/zha/test_gateway.py
@@ -176,6 +176,24 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord
assert member.device.ieee in [device_light_1.ieee]
+async def test_gateway_create_group_with_id(hass, device_light_1, coordinator):
+ """Test creating a group with a specific ID."""
+ zha_gateway = get_zha_gateway(hass)
+ assert zha_gateway is not None
+ zha_gateway.coordinator_zha_device = coordinator
+ coordinator._zha_gateway = zha_gateway
+ device_light_1._zha_gateway = zha_gateway
+
+ zha_group = await zha_gateway.async_create_zigpy_group(
+ "Test Group", [GroupMember(device_light_1.ieee, 1)], group_id=0x1234
+ )
+ await hass.async_block_till_done()
+
+ assert len(zha_group.members) == 1
+ assert zha_group.members[0].device is device_light_1
+ assert zha_group.group_id == 0x1234
+
+
async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic):
"""Test saving data after a delay."""
zha_gateway = get_zha_gateway(hass)
diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py
index caddbb050a5..f0c69709031 100644
--- a/tests/components/zwave_js/conftest.py
+++ b/tests/components/zwave_js/conftest.py
@@ -60,9 +60,14 @@ def mock_addon_options(addon_info):
@pytest.fixture(name="set_addon_options_side_effect")
-def set_addon_options_side_effect_fixture():
+def set_addon_options_side_effect_fixture(addon_options):
"""Return the set add-on options side effect."""
- return None
+
+ async def set_addon_options(hass, slug, options):
+ """Mock set add-on options."""
+ addon_options.update(options["options"])
+
+ return set_addon_options
@pytest.fixture(name="set_addon_options")
@@ -75,11 +80,24 @@ def mock_set_addon_options(set_addon_options_side_effect):
yield set_options
+@pytest.fixture(name="install_addon_side_effect")
+def install_addon_side_effect_fixture(addon_info):
+ """Return the install add-on side effect."""
+
+ async def install_addon(hass, slug):
+ """Mock install add-on."""
+ addon_info.return_value["state"] = "stopped"
+ addon_info.return_value["version"] = "1.0"
+
+ return install_addon
+
+
@pytest.fixture(name="install_addon")
-def mock_install_addon():
+def mock_install_addon(install_addon_side_effect):
"""Mock install add-on."""
with patch(
- "homeassistant.components.zwave_js.addon.async_install_addon"
+ "homeassistant.components.zwave_js.addon.async_install_addon",
+ side_effect=install_addon_side_effect,
) as install_addon:
yield install_addon
@@ -94,9 +112,14 @@ def mock_update_addon():
@pytest.fixture(name="start_addon_side_effect")
-def start_addon_side_effect_fixture():
- """Return the set add-on options side effect."""
- return None
+def start_addon_side_effect_fixture(addon_info):
+ """Return the start add-on options side effect."""
+
+ async def start_addon(hass, slug):
+ """Mock start add-on."""
+ addon_info.return_value["state"] = "started"
+
+ return start_addon
@pytest.fixture(name="start_addon")
@@ -118,6 +141,22 @@ def stop_addon_fixture():
yield stop_addon
+@pytest.fixture(name="restart_addon_side_effect")
+def restart_addon_side_effect_fixture():
+ """Return the restart add-on options side effect."""
+ return None
+
+
+@pytest.fixture(name="restart_addon")
+def mock_restart_addon(restart_addon_side_effect):
+ """Mock restart add-on."""
+ with patch(
+ "homeassistant.components.zwave_js.addon.async_restart_addon",
+ side_effect=restart_addon_side_effect,
+ ) as restart_addon:
+ yield restart_addon
+
+
@pytest.fixture(name="uninstall_addon")
def uninstall_addon_fixture():
"""Mock uninstall add-on."""
diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py
index c498c4201ae..b6d846898d3 100644
--- a/tests/components/zwave_js/test_api.py
+++ b/tests/components/zwave_js/test_api.py
@@ -119,6 +119,54 @@ async def test_node_status(hass, multisensor_6, integration, hass_ws_client):
assert msg["error"]["code"] == ERR_NOT_LOADED
+async def test_node_state(hass, multisensor_6, integration, hass_ws_client):
+ """Test the node_state websocket command."""
+ entry = integration
+ ws_client = await hass_ws_client(hass)
+
+ node = multisensor_6
+ await ws_client.send_json(
+ {
+ ID: 3,
+ TYPE: "zwave_js/node_state",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: node.node_id,
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert msg["result"] == node.data
+
+ # Test getting non-existent node fails
+ await ws_client.send_json(
+ {
+ ID: 4,
+ TYPE: "zwave_js/node_state",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: 99999,
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert not msg["success"]
+ assert msg["error"]["code"] == ERR_NOT_FOUND
+
+ # Test sending command with not loaded entry fails
+ await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+
+ await ws_client.send_json(
+ {
+ ID: 5,
+ TYPE: "zwave_js/node_state",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: node.node_id,
+ }
+ )
+ msg = await ws_client.receive_json()
+
+ assert not msg["success"]
+ assert msg["error"]["code"] == ERR_NOT_LOADED
+
+
async def test_node_metadata(hass, wallmote_central_scene, integration, hass_ws_client):
"""Test the node metadata websocket command."""
entry = integration
@@ -1131,13 +1179,62 @@ async def test_set_config_parameter(
client.async_send_command_no_wait.reset_mock()
+ # Test that hex strings are accepted and converted as expected
+ client.async_send_command_no_wait.return_value = None
+
+ await ws_client.send_json(
+ {
+ ID: 2,
+ TYPE: "zwave_js/set_config_parameter",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: 52,
+ PROPERTY: 102,
+ PROPERTY_KEY: 1,
+ VALUE: "0x1",
+ }
+ )
+
+ msg = await ws_client.receive_json()
+ assert msg["success"]
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClassName": "Configuration",
+ "commandClass": 112,
+ "endpoint": 0,
+ "property": 102,
+ "propertyName": "Group 2: Send battery reports",
+ "propertyKey": 1,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "valueSize": 4,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": True,
+ "label": "Group 2: Send battery reports",
+ "description": "Include battery information in periodic reports to Group 2",
+ "isFromConfig": True,
+ },
+ "value": 0,
+ }
+ assert args["value"] == 1
+
+ client.async_send_command_no_wait.reset_mock()
+
with patch(
"homeassistant.components.zwave_js.api.async_set_config_parameter",
) as set_param_mock:
set_param_mock.side_effect = InvalidNewValue("test")
await ws_client.send_json(
{
- ID: 2,
+ ID: 3,
TYPE: "zwave_js/set_config_parameter",
ENTRY_ID: entry.entry_id,
NODE_ID: 52,
@@ -1157,7 +1254,7 @@ async def test_set_config_parameter(
set_param_mock.side_effect = NotFoundError("test")
await ws_client.send_json(
{
- ID: 3,
+ ID: 4,
TYPE: "zwave_js/set_config_parameter",
ENTRY_ID: entry.entry_id,
NODE_ID: 52,
@@ -1177,7 +1274,7 @@ async def test_set_config_parameter(
set_param_mock.side_effect = SetValueFailed("test")
await ws_client.send_json(
{
- ID: 4,
+ ID: 5,
TYPE: "zwave_js/set_config_parameter",
ENTRY_ID: entry.entry_id,
NODE_ID: 52,
@@ -1197,7 +1294,7 @@ async def test_set_config_parameter(
# Test getting non-existent node fails
await ws_client.send_json(
{
- ID: 5,
+ ID: 6,
TYPE: "zwave_js/set_config_parameter",
ENTRY_ID: entry.entry_id,
NODE_ID: 9999,
@@ -1216,7 +1313,7 @@ async def test_set_config_parameter(
await ws_client.send_json(
{
- ID: 6,
+ ID: 7,
TYPE: "zwave_js/set_config_parameter",
ENTRY_ID: entry.entry_id,
NODE_ID: 52,
@@ -1304,6 +1401,57 @@ async def test_dump_view(integration, hass_client):
assert json.loads(await resp.text()) == [{"hello": "world"}, {"second": "msg"}]
+async def test_version_info(hass, integration, hass_ws_client, version_state):
+ """Test the HTTP dump node view."""
+ entry = integration
+ ws_client = await hass_ws_client(hass)
+
+ version_info = {
+ "driver_version": version_state["driverVersion"],
+ "server_version": version_state["serverVersion"],
+ "min_schema_version": 0,
+ "max_schema_version": 0,
+ }
+
+ await ws_client.send_json(
+ {
+ ID: 3,
+ TYPE: "zwave_js/version_info",
+ ENTRY_ID: entry.entry_id,
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert msg["result"] == version_info
+
+ # Test getting non-existent entry fails
+ await ws_client.send_json(
+ {
+ ID: 4,
+ TYPE: "zwave_js/version_info",
+ ENTRY_ID: "INVALID",
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert not msg["success"]
+ assert msg["error"]["code"] == ERR_NOT_FOUND
+
+ # Test sending command with not loaded entry fails
+ await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+
+ await ws_client.send_json(
+ {
+ ID: 5,
+ TYPE: "zwave_js/version_info",
+ ENTRY_ID: entry.entry_id,
+ }
+ )
+ msg = await ws_client.receive_json()
+
+ assert not msg["success"]
+ assert msg["error"]["code"] == ERR_NOT_LOADED
+
+
async def test_firmware_upload_view(
hass, multisensor_6, integration, hass_client, firmware_file
):
@@ -1348,6 +1496,38 @@ async def test_firmware_upload_view_invalid_payload(
assert resp.status == 400
+@pytest.mark.parametrize(
+ "method, url",
+ [("get", "/api/zwave_js/dump/{}")],
+)
+async def test_view_non_admin_user(
+ integration, hass_client, hass_admin_user, method, url
+):
+ """Test config entry level views for non-admin users."""
+ client = await hass_client()
+ # Verify we require admin user
+ hass_admin_user.groups = []
+ resp = await client.request(method, url.format(integration.entry_id))
+ assert resp.status == 401
+
+
+@pytest.mark.parametrize(
+ "method, url",
+ [("post", "/api/zwave_js/firmware/upload/{}/{}")],
+)
+async def test_node_view_non_admin_user(
+ multisensor_6, integration, hass_client, hass_admin_user, method, url
+):
+ """Test node level views for non-admin users."""
+ client = await hass_client()
+ # Verify we require admin user
+ hass_admin_user.groups = []
+ resp = await client.request(
+ method, url.format(integration.entry_id, multisensor_6.node_id)
+ )
+ assert resp.status == 401
+
+
@pytest.mark.parametrize(
"method, url",
[
@@ -1363,7 +1543,8 @@ async def test_view_invalid_entry_id(integration, hass_client, method, url):
@pytest.mark.parametrize(
- "method, url", [("post", "/api/zwave_js/firmware/upload/{}/111")]
+ "method, url",
+ [("post", "/api/zwave_js/firmware/upload/{}/111")],
)
async def test_view_invalid_node_id(integration, hass_client, method, url):
"""Test an invalid config entry id parameter."""
@@ -1372,15 +1553,15 @@ async def test_view_invalid_node_id(integration, hass_client, method, url):
assert resp.status == 404
-async def test_subscribe_logs(hass, integration, client, hass_ws_client):
- """Test the subscribe_logs websocket command."""
+async def test_subscribe_log_updates(hass, integration, client, hass_ws_client):
+ """Test the subscribe_log_updates websocket command."""
entry = integration
ws_client = await hass_ws_client(hass)
client.async_send_command.return_value = {}
await ws_client.send_json(
- {ID: 1, TYPE: "zwave_js/subscribe_logs", ENTRY_ID: entry.entry_id}
+ {ID: 1, TYPE: "zwave_js/subscribe_log_updates", ENTRY_ID: entry.entry_id}
)
msg = await ws_client.receive_json()
@@ -1407,10 +1588,41 @@ async def test_subscribe_logs(hass, integration, client, hass_ws_client):
msg = await ws_client.receive_json()
assert msg["event"] == {
- "message": ["test"],
- "level": "debug",
- "primary_tags": "tag",
- "timestamp": "time",
+ "type": "log_message",
+ "log_message": {
+ "message": ["test"],
+ "level": "debug",
+ "primary_tags": "tag",
+ "timestamp": "time",
+ },
+ }
+
+ event = Event(
+ type="log config updated",
+ data={
+ "source": "driver",
+ "event": "log config updated",
+ "config": {
+ "enabled": False,
+ "level": "error",
+ "logToFile": True,
+ "filename": "test",
+ "forceConsole": True,
+ },
+ },
+ )
+ client.driver.receive_event(event)
+
+ msg = await ws_client.receive_json()
+ assert msg["event"] == {
+ "type": "log_config",
+ "log_config": {
+ "enabled": False,
+ "level": "error",
+ "log_to_file": True,
+ "filename": "test",
+ "force_console": True,
+ },
}
# Test sending command with not loaded entry fails
@@ -1418,7 +1630,7 @@ async def test_subscribe_logs(hass, integration, client, hass_ws_client):
await hass.async_block_till_done()
await ws_client.send_json(
- {ID: 2, TYPE: "zwave_js/subscribe_logs", ENTRY_ID: entry.entry_id}
+ {ID: 2, TYPE: "zwave_js/subscribe_log_updates", ENTRY_ID: entry.entry_id}
)
msg = await ws_client.receive_json()
@@ -1569,16 +1781,6 @@ async def test_get_log_config(hass, client, integration, hass_ws_client):
ws_client = await hass_ws_client(hass)
# Test we can get log configuration
- client.async_send_command.return_value = {
- "success": True,
- "config": {
- "enabled": True,
- "level": "error",
- "logToFile": False,
- "filename": "/test.txt",
- "forceConsole": False,
- },
- }
await ws_client.send_json(
{
ID: 1,
@@ -1592,9 +1794,9 @@ async def test_get_log_config(hass, client, integration, hass_ws_client):
log_config = msg["result"]
assert log_config["enabled"]
- assert log_config["level"] == LogLevel.ERROR
+ assert log_config["level"] == LogLevel.INFO
assert log_config["log_to_file"] is False
- assert log_config["filename"] == "/test.txt"
+ assert log_config["filename"] == ""
assert log_config["force_console"] is False
# Test sending command with not loaded entry fails
diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py
index f86052b3692..fefa680ce77 100644
--- a/tests/components/zwave_js/test_climate.py
+++ b/tests/components/zwave_js/test_climate.py
@@ -382,6 +382,30 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat
blocking=True,
)
+ # Test setting illegal mode raises an error
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {
+ ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY,
+ ATTR_HVAC_MODE: HVAC_MODE_COOL,
+ },
+ blocking=True,
+ )
+
+ # Test that setting HVAC_MODE_HEAT works. If the no-op logic didn't work, this would
+ # raise an error
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {
+ ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY,
+ ATTR_HVAC_MODE: HVAC_MODE_HEAT,
+ },
+ blocking=True,
+ )
+
assert len(client.async_send_command_no_wait.call_args_list) == 1
args = client.async_send_command_no_wait.call_args_list[0][0][0]
assert args["command"] == "node.set_value"
diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py
index 2414d2aea00..7d02c215d45 100644
--- a/tests/components/zwave_js/test_config_flow.py
+++ b/tests/components/zwave_js/test_config_flow.py
@@ -2,6 +2,7 @@
import asyncio
from unittest.mock import DEFAULT, call, patch
+import aiohttp
import pytest
from zwave_js_server.version import VersionInfo
@@ -19,6 +20,21 @@ ADDON_DISCOVERY_INFO = {
}
+@pytest.fixture(name="persistent_notification", autouse=True)
+async def setup_persistent_notification(hass):
+ """Set up persistent notification integration."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+
+@pytest.fixture(name="setup_entry")
+def setup_entry_fixture():
+ """Mock entry setup."""
+ with patch(
+ "homeassistant.components.zwave_js.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ yield mock_setup_entry
+
+
@pytest.fixture(name="supervisor")
def mock_supervisor_fixture():
"""Mock Supervisor."""
@@ -134,6 +150,19 @@ async def slow_server_version(*args):
await asyncio.sleep(0.1)
+@pytest.mark.parametrize(
+ "flow, flow_params",
+ [
+ (
+ "flow",
+ lambda entry: {
+ "handler": DOMAIN,
+ "context": {"source": config_entries.SOURCE_USER},
+ },
+ ),
+ ("options", lambda entry: {"handler": entry.entry_id}),
+ ],
+)
@pytest.mark.parametrize(
"url, server_version_side_effect, server_version_timeout, error",
[
@@ -157,20 +186,15 @@ async def slow_server_version(*args):
),
],
)
-async def test_manual_errors(
- hass,
- url,
- error,
-):
+async def test_manual_errors(hass, integration, url, error, flow, flow_params):
"""Test all errors with a manual set up."""
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
+ entry = integration
+ result = await getattr(hass.config_entries, flow).async_init(**flow_params(entry))
assert result["type"] == "form"
assert result["step_id"] == "manual"
- result = await hass.config_entries.flow.async_configure(
+ result = await getattr(hass.config_entries, flow).async_configure(
result["flow_id"],
{
"url": url,
@@ -1059,3 +1083,714 @@ async def test_install_addon_failure(hass, supervisor, addon_installed, install_
assert result["type"] == "abort"
assert result["reason"] == "addon_install_failed"
+
+
+async def test_options_manual(hass, client, integration):
+ """Test manual settings in options flow."""
+ entry = integration
+ entry.unique_id = 1234
+
+ assert client.connect.call_count == 1
+ assert client.disconnect.call_count == 0
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "manual"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], {"url": "ws://1.1.1.1:3001"}
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "create_entry"
+ assert entry.data["url"] == "ws://1.1.1.1:3001"
+ assert entry.data["use_addon"] is False
+ assert entry.data["integration_created_addon"] is False
+ assert client.connect.call_count == 2
+ assert client.disconnect.call_count == 1
+
+
+async def test_options_manual_different_device(hass, integration):
+ """Test options flow manual step connecting to different device."""
+ entry = integration
+ entry.unique_id = 5678
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "manual"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], {"url": "ws://1.1.1.1:3001"}
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "different_device"
+
+
+async def test_options_not_addon(hass, client, supervisor, integration):
+ """Test options flow and opting out of add-on on Supervisor."""
+ entry = integration
+ entry.unique_id = 1234
+
+ assert client.connect.call_count == 1
+ assert client.disconnect.call_count == 0
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "on_supervisor"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], {"use_addon": False}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "manual"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ {
+ "url": "ws://localhost:3000",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "create_entry"
+ assert entry.data["url"] == "ws://localhost:3000"
+ assert entry.data["use_addon"] is False
+ assert entry.data["integration_created_addon"] is False
+ assert client.connect.call_count == 2
+ assert client.disconnect.call_count == 1
+
+
+@pytest.mark.parametrize(
+ "discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls",
+ [
+ (
+ {"config": ADDON_DISCOVERY_INFO},
+ {},
+ {"device": "/test", "network_key": "abc123"},
+ {
+ "usb_path": "/new",
+ "network_key": "new123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ 0,
+ ),
+ (
+ {"config": ADDON_DISCOVERY_INFO},
+ {"use_addon": True},
+ {"device": "/test", "network_key": "abc123"},
+ {
+ "usb_path": "/new",
+ "network_key": "new123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ 1,
+ ),
+ ],
+)
+async def test_options_addon_running(
+ hass,
+ client,
+ supervisor,
+ integration,
+ addon_running,
+ addon_options,
+ set_addon_options,
+ restart_addon,
+ get_addon_discovery_info,
+ discovery_info,
+ entry_data,
+ old_addon_options,
+ new_addon_options,
+ disconnect_calls,
+):
+ """Test options flow and add-on already running on Supervisor."""
+ addon_options.update(old_addon_options)
+ entry = integration
+ entry.unique_id = 1234
+ data = {**entry.data, **entry_data}
+ hass.config_entries.async_update_entry(entry, data=data)
+
+ assert entry.data["url"] == "ws://test.org"
+
+ assert client.connect.call_count == 1
+ assert client.disconnect.call_count == 0
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "on_supervisor"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], {"use_addon": True}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "configure_addon"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ new_addon_options,
+ )
+
+ new_addon_options["device"] = new_addon_options.pop("usb_path")
+ assert set_addon_options.call_args == call(
+ hass,
+ "core_zwave_js",
+ {"options": new_addon_options},
+ )
+ assert client.disconnect.call_count == disconnect_calls
+
+ assert result["type"] == "progress"
+ assert result["step_id"] == "start_addon"
+
+ await hass.async_block_till_done()
+ result = await hass.config_entries.options.async_configure(result["flow_id"])
+ await hass.async_block_till_done()
+
+ assert restart_addon.call_args == call(hass, "core_zwave_js")
+
+ assert result["type"] == "create_entry"
+ assert entry.data["url"] == "ws://host1:3001"
+ assert entry.data["usb_path"] == new_addon_options["device"]
+ assert entry.data["network_key"] == new_addon_options["network_key"]
+ assert entry.data["use_addon"] is True
+ assert entry.data["integration_created_addon"] is False
+ assert client.connect.call_count == 2
+ assert client.disconnect.call_count == 1
+
+
+@pytest.mark.parametrize(
+ "discovery_info, entry_data, old_addon_options, new_addon_options",
+ [
+ (
+ {"config": ADDON_DISCOVERY_INFO},
+ {},
+ {
+ "device": "/test",
+ "network_key": "abc123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ {
+ "usb_path": "/test",
+ "network_key": "abc123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ ),
+ ],
+)
+async def test_options_addon_running_no_changes(
+ hass,
+ client,
+ supervisor,
+ integration,
+ addon_running,
+ addon_options,
+ set_addon_options,
+ restart_addon,
+ get_addon_discovery_info,
+ discovery_info,
+ entry_data,
+ old_addon_options,
+ new_addon_options,
+):
+ """Test options flow without changes, and add-on already running on Supervisor."""
+ addon_options.update(old_addon_options)
+ entry = integration
+ entry.unique_id = 1234
+ data = {**entry.data, **entry_data}
+ hass.config_entries.async_update_entry(entry, data=data)
+
+ assert entry.data["url"] == "ws://test.org"
+
+ assert client.connect.call_count == 1
+ assert client.disconnect.call_count == 0
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "on_supervisor"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], {"use_addon": True}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "configure_addon"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ new_addon_options,
+ )
+ await hass.async_block_till_done()
+
+ new_addon_options["device"] = new_addon_options.pop("usb_path")
+ assert set_addon_options.call_count == 0
+ assert restart_addon.call_count == 0
+
+ assert result["type"] == "create_entry"
+ assert entry.data["url"] == "ws://host1:3001"
+ assert entry.data["usb_path"] == new_addon_options["device"]
+ assert entry.data["network_key"] == new_addon_options["network_key"]
+ assert entry.data["use_addon"] is True
+ assert entry.data["integration_created_addon"] is False
+ assert client.connect.call_count == 2
+ assert client.disconnect.call_count == 1
+
+
+async def different_device_server_version(*args):
+ """Return server version for a device with different home id."""
+ return VersionInfo(
+ driver_version="mock-driver-version",
+ server_version="mock-server-version",
+ home_id=5678,
+ min_schema_version=0,
+ max_schema_version=1,
+ )
+
+
+@pytest.mark.parametrize(
+ "discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls, server_version_side_effect",
+ [
+ (
+ {"config": ADDON_DISCOVERY_INFO},
+ {},
+ {
+ "device": "/test",
+ "network_key": "abc123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ {
+ "usb_path": "/new",
+ "network_key": "new123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ 0,
+ different_device_server_version,
+ ),
+ ],
+)
+async def test_options_different_device(
+ hass,
+ client,
+ supervisor,
+ integration,
+ addon_running,
+ addon_options,
+ set_addon_options,
+ restart_addon,
+ get_addon_discovery_info,
+ discovery_info,
+ entry_data,
+ old_addon_options,
+ new_addon_options,
+ disconnect_calls,
+ server_version_side_effect,
+):
+ """Test options flow and configuring a different device."""
+ addon_options.update(old_addon_options)
+ entry = integration
+ entry.unique_id = 1234
+ data = {**entry.data, **entry_data}
+ hass.config_entries.async_update_entry(entry, data=data)
+
+ assert entry.data["url"] == "ws://test.org"
+
+ assert client.connect.call_count == 1
+ assert client.disconnect.call_count == 0
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "on_supervisor"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], {"use_addon": True}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "configure_addon"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ new_addon_options,
+ )
+
+ assert set_addon_options.call_count == 1
+ new_addon_options["device"] = new_addon_options.pop("usb_path")
+ assert set_addon_options.call_args == call(
+ hass,
+ "core_zwave_js",
+ {"options": new_addon_options},
+ )
+ assert client.disconnect.call_count == disconnect_calls
+ assert result["type"] == "progress"
+ assert result["step_id"] == "start_addon"
+
+ await hass.async_block_till_done()
+
+ assert restart_addon.call_count == 1
+ assert restart_addon.call_args == call(hass, "core_zwave_js")
+
+ result = await hass.config_entries.options.async_configure(result["flow_id"])
+ await hass.async_block_till_done()
+
+ assert set_addon_options.call_count == 2
+ assert set_addon_options.call_args == call(
+ hass,
+ "core_zwave_js",
+ {"options": old_addon_options},
+ )
+ assert result["type"] == "progress"
+ assert result["step_id"] == "start_addon"
+
+ await hass.async_block_till_done()
+
+ assert restart_addon.call_count == 2
+ assert restart_addon.call_args == call(hass, "core_zwave_js")
+
+ result = await hass.config_entries.options.async_configure(result["flow_id"])
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "different_device"
+ assert entry.data == data
+ assert client.connect.call_count == 2
+ assert client.disconnect.call_count == 1
+
+
+@pytest.mark.parametrize(
+ "discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls, restart_addon_side_effect",
+ [
+ (
+ {"config": ADDON_DISCOVERY_INFO},
+ {},
+ {
+ "device": "/test",
+ "network_key": "abc123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ {
+ "usb_path": "/new",
+ "network_key": "new123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ 0,
+ [HassioAPIError(), None],
+ ),
+ (
+ {"config": ADDON_DISCOVERY_INFO},
+ {},
+ {
+ "device": "/test",
+ "network_key": "abc123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ {
+ "usb_path": "/new",
+ "network_key": "new123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ 0,
+ [
+ HassioAPIError(),
+ HassioAPIError(),
+ ],
+ ),
+ ],
+)
+async def test_options_addon_restart_failed(
+ hass,
+ client,
+ supervisor,
+ integration,
+ addon_running,
+ addon_options,
+ set_addon_options,
+ restart_addon,
+ get_addon_discovery_info,
+ discovery_info,
+ entry_data,
+ old_addon_options,
+ new_addon_options,
+ disconnect_calls,
+ restart_addon_side_effect,
+):
+ """Test options flow and add-on restart failure."""
+ addon_options.update(old_addon_options)
+ entry = integration
+ entry.unique_id = 1234
+ data = {**entry.data, **entry_data}
+ hass.config_entries.async_update_entry(entry, data=data)
+
+ assert entry.data["url"] == "ws://test.org"
+
+ assert client.connect.call_count == 1
+ assert client.disconnect.call_count == 0
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "on_supervisor"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], {"use_addon": True}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "configure_addon"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ new_addon_options,
+ )
+
+ assert set_addon_options.call_count == 1
+ new_addon_options["device"] = new_addon_options.pop("usb_path")
+ assert set_addon_options.call_args == call(
+ hass,
+ "core_zwave_js",
+ {"options": new_addon_options},
+ )
+ assert client.disconnect.call_count == disconnect_calls
+ assert result["type"] == "progress"
+ assert result["step_id"] == "start_addon"
+
+ await hass.async_block_till_done()
+
+ assert restart_addon.call_count == 1
+ assert restart_addon.call_args == call(hass, "core_zwave_js")
+
+ result = await hass.config_entries.options.async_configure(result["flow_id"])
+ await hass.async_block_till_done()
+
+ assert set_addon_options.call_count == 2
+ assert set_addon_options.call_args == call(
+ hass,
+ "core_zwave_js",
+ {"options": old_addon_options},
+ )
+ assert result["type"] == "progress"
+ assert result["step_id"] == "start_addon"
+
+ await hass.async_block_till_done()
+
+ assert restart_addon.call_count == 2
+ assert restart_addon.call_args == call(hass, "core_zwave_js")
+
+ result = await hass.config_entries.options.async_configure(result["flow_id"])
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "addon_start_failed"
+ assert entry.data == data
+ assert client.connect.call_count == 2
+ assert client.disconnect.call_count == 1
+
+
+@pytest.mark.parametrize(
+ "discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls, server_version_side_effect",
+ [
+ (
+ {"config": ADDON_DISCOVERY_INFO},
+ {},
+ {
+ "device": "/test",
+ "network_key": "abc123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ {
+ "usb_path": "/test",
+ "network_key": "abc123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ 0,
+ aiohttp.ClientError("Boom"),
+ ),
+ ],
+)
+async def test_options_addon_running_server_info_failure(
+ hass,
+ client,
+ supervisor,
+ integration,
+ addon_running,
+ addon_options,
+ set_addon_options,
+ restart_addon,
+ get_addon_discovery_info,
+ discovery_info,
+ entry_data,
+ old_addon_options,
+ new_addon_options,
+ disconnect_calls,
+ server_version_side_effect,
+):
+ """Test options flow and add-on already running with server info failure."""
+ addon_options.update(old_addon_options)
+ entry = integration
+ entry.unique_id = 1234
+ data = {**entry.data, **entry_data}
+ hass.config_entries.async_update_entry(entry, data=data)
+
+ assert entry.data["url"] == "ws://test.org"
+
+ assert client.connect.call_count == 1
+ assert client.disconnect.call_count == 0
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "on_supervisor"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], {"use_addon": True}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "configure_addon"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ new_addon_options,
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "cannot_connect"
+ assert entry.data == data
+ assert client.connect.call_count == 2
+ assert client.disconnect.call_count == 1
+
+
+@pytest.mark.parametrize(
+ "discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls",
+ [
+ (
+ {"config": ADDON_DISCOVERY_INFO},
+ {},
+ {"device": "/test", "network_key": "abc123"},
+ {
+ "usb_path": "/new",
+ "network_key": "new123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ 0,
+ ),
+ (
+ {"config": ADDON_DISCOVERY_INFO},
+ {"use_addon": True},
+ {"device": "/test", "network_key": "abc123"},
+ {
+ "usb_path": "/new",
+ "network_key": "new123",
+ "log_level": "info",
+ "emulate_hardware": False,
+ },
+ 1,
+ ),
+ ],
+)
+async def test_options_addon_not_installed(
+ hass,
+ client,
+ supervisor,
+ addon_installed,
+ install_addon,
+ integration,
+ addon_options,
+ set_addon_options,
+ start_addon,
+ get_addon_discovery_info,
+ discovery_info,
+ entry_data,
+ old_addon_options,
+ new_addon_options,
+ disconnect_calls,
+):
+ """Test options flow and add-on not installed on Supervisor."""
+ addon_installed.return_value["version"] = None
+ addon_options.update(old_addon_options)
+ entry = integration
+ entry.unique_id = 1234
+ data = {**entry.data, **entry_data}
+ hass.config_entries.async_update_entry(entry, data=data)
+
+ assert entry.data["url"] == "ws://test.org"
+
+ assert client.connect.call_count == 1
+ assert client.disconnect.call_count == 0
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "on_supervisor"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], {"use_addon": True}
+ )
+
+ assert result["type"] == "progress"
+ assert result["step_id"] == "install_addon"
+
+ # Make sure the flow continues when the progress task is done.
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_configure(result["flow_id"])
+
+ assert install_addon.call_args == call(hass, "core_zwave_js")
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "configure_addon"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ new_addon_options,
+ )
+
+ new_addon_options["device"] = new_addon_options.pop("usb_path")
+ assert set_addon_options.call_args == call(
+ hass,
+ "core_zwave_js",
+ {"options": new_addon_options},
+ )
+ assert client.disconnect.call_count == disconnect_calls
+
+ assert result["type"] == "progress"
+ assert result["step_id"] == "start_addon"
+
+ await hass.async_block_till_done()
+
+ assert start_addon.call_count == 1
+ assert start_addon.call_args == call(hass, "core_zwave_js")
+
+ result = await hass.config_entries.options.async_configure(result["flow_id"])
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ assert result["type"] == "create_entry"
+ assert entry.data["url"] == "ws://host1:3001"
+ assert entry.data["usb_path"] == new_addon_options["device"]
+ assert entry.data["network_key"] == new_addon_options["network_key"]
+ assert entry.data["use_addon"] is True
+ assert entry.data["integration_created_addon"] is True
+ assert client.connect.call_count == 2
+ assert client.disconnect.call_count == 1
diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py
index 67d5a416a91..0b9009cd1d7 100644
--- a/tests/components/zwave_js/test_init.py
+++ b/tests/components/zwave_js/test_init.py
@@ -13,11 +13,7 @@ from homeassistant.config_entries import DISABLED_USER, ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.helpers import device_registry as dr, entity_registry as er
-from .common import (
- AIR_TEMPERATURE_SENSOR,
- EATON_RF9640_ENTITY,
- NOTIFICATION_MOTION_BINARY_SENSOR,
-)
+from .common import AIR_TEMPERATURE_SENSOR, EATON_RF9640_ENTITY
from tests.common import MockConfigEntry
@@ -152,324 +148,6 @@ async def test_on_node_added_ready(hass, multisensor_6_state, client, integratio
assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)})
-async def test_unique_id_migration_dupes(
- hass, multisensor_6_state, client, integration
-):
- """Test we remove an entity when ."""
- ent_reg = er.async_get(hass)
-
- entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1]
-
- # Create entity RegistryEntry using old unique ID format
- old_unique_id_1 = (
- f"{client.driver.controller.home_id}.52.52-49-00-Air temperature-00"
- )
- entity_entry = ent_reg.async_get_or_create(
- "sensor",
- DOMAIN,
- old_unique_id_1,
- suggested_object_id=entity_name,
- config_entry=integration,
- original_name=entity_name,
- )
- assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR
- assert entity_entry.unique_id == old_unique_id_1
-
- # Create entity RegistryEntry using b0 unique ID format
- old_unique_id_2 = (
- f"{client.driver.controller.home_id}.52.52-49-0-Air temperature-00-00"
- )
- entity_entry = ent_reg.async_get_or_create(
- "sensor",
- DOMAIN,
- old_unique_id_2,
- suggested_object_id=f"{entity_name}_1",
- config_entry=integration,
- original_name=entity_name,
- )
- assert entity_entry.entity_id == f"{AIR_TEMPERATURE_SENSOR}_1"
- assert entity_entry.unique_id == old_unique_id_2
-
- # Add a ready node, unique ID should be migrated
- node = Node(client, multisensor_6_state)
- event = {"node": node}
-
- client.driver.controller.emit("node added", event)
- await hass.async_block_till_done()
-
- # Check that new RegistryEntry is using new unique ID format
- entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
- new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature"
- assert entity_entry.unique_id == new_unique_id
- assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1) is None
- assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_2) is None
-
-
-@pytest.mark.parametrize(
- "id",
- [
- ("52.52-49-00-Air temperature-00"),
- ("52.52-49-0-Air temperature-00-00"),
- ("52-49-0-Air temperature-00-00"),
- ],
-)
-async def test_unique_id_migration(hass, multisensor_6_state, client, integration, id):
- """Test unique ID is migrated from old format to new."""
- ent_reg = er.async_get(hass)
-
- # Migrate version 1
- entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1]
-
- # Create entity RegistryEntry using old unique ID format
- old_unique_id = f"{client.driver.controller.home_id}.{id}"
- entity_entry = ent_reg.async_get_or_create(
- "sensor",
- DOMAIN,
- old_unique_id,
- suggested_object_id=entity_name,
- config_entry=integration,
- original_name=entity_name,
- )
- assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR
- assert entity_entry.unique_id == old_unique_id
-
- # Add a ready node, unique ID should be migrated
- node = Node(client, multisensor_6_state)
- event = {"node": node}
-
- client.driver.controller.emit("node added", event)
- await hass.async_block_till_done()
-
- # Check that new RegistryEntry is using new unique ID format
- entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
- new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature"
- assert entity_entry.unique_id == new_unique_id
- assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None
-
-
-@pytest.mark.parametrize(
- "id",
- [
- ("32.32-50-00-value-W_Consumed"),
- ("32.32-50-0-value-66049-W_Consumed"),
- ("32-50-0-value-66049-W_Consumed"),
- ],
-)
-async def test_unique_id_migration_property_key(
- hass, hank_binary_switch_state, client, integration, id
-):
- """Test unique ID with property key is migrated from old format to new."""
- ent_reg = er.async_get(hass)
-
- SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
- entity_name = SENSOR_NAME.split(".")[1]
-
- # Create entity RegistryEntry using old unique ID format
- old_unique_id = f"{client.driver.controller.home_id}.{id}"
- entity_entry = ent_reg.async_get_or_create(
- "sensor",
- DOMAIN,
- old_unique_id,
- suggested_object_id=entity_name,
- config_entry=integration,
- original_name=entity_name,
- )
- assert entity_entry.entity_id == SENSOR_NAME
- assert entity_entry.unique_id == old_unique_id
-
- # Add a ready node, unique ID should be migrated
- node = Node(client, hank_binary_switch_state)
- event = {"node": node}
-
- client.driver.controller.emit("node added", event)
- await hass.async_block_till_done()
-
- # Check that new RegistryEntry is using new unique ID format
- entity_entry = ent_reg.async_get(SENSOR_NAME)
- new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
- assert entity_entry.unique_id == new_unique_id
- assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None
-
-
-async def test_unique_id_migration_notification_binary_sensor(
- hass, multisensor_6_state, client, integration
-):
- """Test unique ID is migrated from old format to new for a notification binary sensor."""
- ent_reg = er.async_get(hass)
-
- entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1]
-
- # Create entity RegistryEntry using old unique ID format
- old_unique_id = f"{client.driver.controller.home_id}.52.52-113-00-Home Security-Motion sensor status.8"
- entity_entry = ent_reg.async_get_or_create(
- "binary_sensor",
- DOMAIN,
- old_unique_id,
- suggested_object_id=entity_name,
- config_entry=integration,
- original_name=entity_name,
- )
- assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR
- assert entity_entry.unique_id == old_unique_id
-
- # Add a ready node, unique ID should be migrated
- node = Node(client, multisensor_6_state)
- event = {"node": node}
-
- client.driver.controller.emit("node added", event)
- await hass.async_block_till_done()
-
- # Check that new RegistryEntry is using new unique ID format
- entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR)
- new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8"
- assert entity_entry.unique_id == new_unique_id
- assert ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None
-
-
-async def test_old_entity_migration(
- hass, hank_binary_switch_state, client, integration
-):
- """Test old entity on a different endpoint is migrated to a new one."""
- node = Node(client, hank_binary_switch_state)
-
- ent_reg = er.async_get(hass)
- dev_reg = dr.async_get(hass)
- device = dev_reg.async_get_or_create(
- config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)}
- )
-
- SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
- entity_name = SENSOR_NAME.split(".")[1]
-
- # Create entity RegistryEntry using fake endpoint
- old_unique_id = f"{client.driver.controller.home_id}.32-50-1-value-66049"
- entity_entry = ent_reg.async_get_or_create(
- "sensor",
- DOMAIN,
- old_unique_id,
- suggested_object_id=entity_name,
- config_entry=integration,
- original_name=entity_name,
- device_id=device.id,
- )
- assert entity_entry.entity_id == SENSOR_NAME
- assert entity_entry.unique_id == old_unique_id
-
- # Do this twice to make sure re-interview doesn't do anything weird
- for i in range(0, 2):
- # Add a ready node, unique ID should be migrated
- event = {"node": node}
- client.driver.controller.emit("node added", event)
- await hass.async_block_till_done()
-
- # Check that new RegistryEntry is using new unique ID format
- entity_entry = ent_reg.async_get(SENSOR_NAME)
- new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
- assert entity_entry.unique_id == new_unique_id
- assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None
-
-
-async def test_skip_old_entity_migration_for_multiple(
- hass, hank_binary_switch_state, client, integration
-):
- """Test that multiple entities of the same value but on a different endpoint get skipped."""
- node = Node(client, hank_binary_switch_state)
-
- ent_reg = er.async_get(hass)
- dev_reg = dr.async_get(hass)
- device = dev_reg.async_get_or_create(
- config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)}
- )
-
- SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
- entity_name = SENSOR_NAME.split(".")[1]
-
- # Create two entity entrrys using different endpoints
- old_unique_id_1 = f"{client.driver.controller.home_id}.32-50-1-value-66049"
- entity_entry = ent_reg.async_get_or_create(
- "sensor",
- DOMAIN,
- old_unique_id_1,
- suggested_object_id=f"{entity_name}_1",
- config_entry=integration,
- original_name=f"{entity_name}_1",
- device_id=device.id,
- )
- assert entity_entry.entity_id == f"{SENSOR_NAME}_1"
- assert entity_entry.unique_id == old_unique_id_1
-
- # Create two entity entrrys using different endpoints
- old_unique_id_2 = f"{client.driver.controller.home_id}.32-50-2-value-66049"
- entity_entry = ent_reg.async_get_or_create(
- "sensor",
- DOMAIN,
- old_unique_id_2,
- suggested_object_id=f"{entity_name}_2",
- config_entry=integration,
- original_name=f"{entity_name}_2",
- device_id=device.id,
- )
- assert entity_entry.entity_id == f"{SENSOR_NAME}_2"
- assert entity_entry.unique_id == old_unique_id_2
- # Add a ready node, unique ID should be migrated
- event = {"node": node}
- client.driver.controller.emit("node added", event)
- await hass.async_block_till_done()
-
- # Check that new RegistryEntry is created using new unique ID format
- entity_entry = ent_reg.async_get(SENSOR_NAME)
- new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
- assert entity_entry.unique_id == new_unique_id
-
- # Check that the old entities stuck around because we skipped the migration step
- assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1)
- assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_2)
-
-
-async def test_old_entity_migration_notification_binary_sensor(
- hass, multisensor_6_state, client, integration
-):
- """Test old entity on a different endpoint is migrated to a new one for a notification binary sensor."""
- node = Node(client, multisensor_6_state)
-
- ent_reg = er.async_get(hass)
- dev_reg = dr.async_get(hass)
- device = dev_reg.async_get_or_create(
- config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)}
- )
-
- entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1]
-
- # Create entity RegistryEntry using old unique ID format
- old_unique_id = f"{client.driver.controller.home_id}.52-113-1-Home Security-Motion sensor status.8"
- entity_entry = ent_reg.async_get_or_create(
- "binary_sensor",
- DOMAIN,
- old_unique_id,
- suggested_object_id=entity_name,
- config_entry=integration,
- original_name=entity_name,
- device_id=device.id,
- )
- assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR
- assert entity_entry.unique_id == old_unique_id
-
- # Do this twice to make sure re-interview doesn't do anything weird
- for _ in range(0, 2):
- # Add a ready node, unique ID should be migrated
- event = {"node": node}
- client.driver.controller.emit("node added", event)
- await hass.async_block_till_done()
-
- # Check that new RegistryEntry is using new unique ID format
- entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR)
- new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8"
- assert entity_entry.unique_id == new_unique_id
- assert (
- ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None
- )
-
-
async def test_on_node_added_not_ready(hass, multisensor_6_state, client, integration):
"""Test we handle a non ready node added event."""
dev_reg = dr.async_get(hass)
@@ -916,7 +594,7 @@ async def test_removed_device(hass, client, multiple_devices, integration):
# Check how many entities there are
ent_reg = er.async_get(hass)
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
- assert len(entity_entries) == 24
+ assert len(entity_entries) == 26
# Remove a node and reload the entry
old_node = nodes.pop(13)
@@ -928,7 +606,7 @@ async def test_removed_device(hass, client, multiple_devices, integration):
device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id)
assert len(device_entries) == 1
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
- assert len(entity_entries) == 15
+ assert len(entity_entries) == 16
assert dev_reg.async_get_device({get_device_id(client, old_node)}) is None
diff --git a/tests/components/zwave_js/test_migrate.py b/tests/components/zwave_js/test_migrate.py
new file mode 100644
index 00000000000..a1f60c31fce
--- /dev/null
+++ b/tests/components/zwave_js/test_migrate.py
@@ -0,0 +1,368 @@
+"""Test the Z-Wave JS migration module."""
+import pytest
+from zwave_js_server.model.node import Node
+
+from homeassistant.components.zwave_js.const import DOMAIN
+from homeassistant.components.zwave_js.helpers import get_device_id
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+
+from .common import AIR_TEMPERATURE_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR
+
+
+async def test_unique_id_migration_dupes(
+ hass, multisensor_6_state, client, integration
+):
+ """Test we remove an entity when ."""
+ ent_reg = er.async_get(hass)
+
+ entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id_1 = (
+ f"{client.driver.controller.home_id}.52.52-49-00-Air temperature-00"
+ )
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id_1,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR
+ assert entity_entry.unique_id == old_unique_id_1
+
+ # Create entity RegistryEntry using b0 unique ID format
+ old_unique_id_2 = (
+ f"{client.driver.controller.home_id}.52.52-49-0-Air temperature-00-00"
+ )
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id_2,
+ suggested_object_id=f"{entity_name}_1",
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == f"{AIR_TEMPERATURE_SENSOR}_1"
+ assert entity_entry.unique_id == old_unique_id_2
+
+ # Add a ready node, unique ID should be migrated
+ node = Node(client, multisensor_6_state)
+ event = {"node": node}
+
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
+ new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature"
+ assert entity_entry.unique_id == new_unique_id
+ assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1) is None
+ assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_2) is None
+
+
+@pytest.mark.parametrize(
+ "id",
+ [
+ ("52.52-49-00-Air temperature-00"),
+ ("52.52-49-0-Air temperature-00-00"),
+ ("52-49-0-Air temperature-00-00"),
+ ],
+)
+async def test_unique_id_migration(hass, multisensor_6_state, client, integration, id):
+ """Test unique ID is migrated from old format to new."""
+ ent_reg = er.async_get(hass)
+
+ # Migrate version 1
+ entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id = f"{client.driver.controller.home_id}.{id}"
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR
+ assert entity_entry.unique_id == old_unique_id
+
+ # Add a ready node, unique ID should be migrated
+ node = Node(client, multisensor_6_state)
+ event = {"node": node}
+
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
+ new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature"
+ assert entity_entry.unique_id == new_unique_id
+ assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None
+
+
+@pytest.mark.parametrize(
+ "id",
+ [
+ ("32.32-50-00-value-W_Consumed"),
+ ("32.32-50-0-value-66049-W_Consumed"),
+ ("32-50-0-value-66049-W_Consumed"),
+ ],
+)
+async def test_unique_id_migration_property_key(
+ hass, hank_binary_switch_state, client, integration, id
+):
+ """Test unique ID with property key is migrated from old format to new."""
+ ent_reg = er.async_get(hass)
+
+ SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
+ entity_name = SENSOR_NAME.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id = f"{client.driver.controller.home_id}.{id}"
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == SENSOR_NAME
+ assert entity_entry.unique_id == old_unique_id
+
+ # Add a ready node, unique ID should be migrated
+ node = Node(client, hank_binary_switch_state)
+ event = {"node": node}
+
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(SENSOR_NAME)
+ new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
+ assert entity_entry.unique_id == new_unique_id
+ assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None
+
+
+async def test_unique_id_migration_notification_binary_sensor(
+ hass, multisensor_6_state, client, integration
+):
+ """Test unique ID is migrated from old format to new for a notification binary sensor."""
+ ent_reg = er.async_get(hass)
+
+ entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id = f"{client.driver.controller.home_id}.52.52-113-00-Home Security-Motion sensor status.8"
+ entity_entry = ent_reg.async_get_or_create(
+ "binary_sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR
+ assert entity_entry.unique_id == old_unique_id
+
+ # Add a ready node, unique ID should be migrated
+ node = Node(client, multisensor_6_state)
+ event = {"node": node}
+
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR)
+ new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8"
+ assert entity_entry.unique_id == new_unique_id
+ assert ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None
+
+
+async def test_old_entity_migration(
+ hass, hank_binary_switch_state, client, integration
+):
+ """Test old entity on a different endpoint is migrated to a new one."""
+ node = Node(client, hank_binary_switch_state)
+
+ ent_reg = er.async_get(hass)
+ dev_reg = dr.async_get(hass)
+ device = dev_reg.async_get_or_create(
+ config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)}
+ )
+
+ SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
+ entity_name = SENSOR_NAME.split(".")[1]
+
+ # Create entity RegistryEntry using fake endpoint
+ old_unique_id = f"{client.driver.controller.home_id}.32-50-1-value-66049"
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ device_id=device.id,
+ )
+ assert entity_entry.entity_id == SENSOR_NAME
+ assert entity_entry.unique_id == old_unique_id
+
+ # Do this twice to make sure re-interview doesn't do anything weird
+ for i in range(0, 2):
+ # Add a ready node, unique ID should be migrated
+ event = {"node": node}
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(SENSOR_NAME)
+ new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
+ assert entity_entry.unique_id == new_unique_id
+ assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None
+
+
+async def test_different_endpoint_migration_status_sensor(
+ hass, hank_binary_switch_state, client, integration
+):
+ """Test that the different endpoint migration logic skips over the status sensor."""
+ node = Node(client, hank_binary_switch_state)
+
+ ent_reg = er.async_get(hass)
+ dev_reg = dr.async_get(hass)
+ device = dev_reg.async_get_or_create(
+ config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)}
+ )
+
+ SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_status_sensor"
+ entity_name = SENSOR_NAME.split(".")[1]
+
+ # Create entity RegistryEntry using fake endpoint
+ old_unique_id = f"{client.driver.controller.home_id}.32.node_status"
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ device_id=device.id,
+ )
+ assert entity_entry.entity_id == SENSOR_NAME
+ assert entity_entry.unique_id == old_unique_id
+
+ # Do this twice to make sure re-interview doesn't do anything weird
+ for i in range(0, 2):
+ # Add a ready node, unique ID should be migrated
+ event = {"node": node}
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that the RegistryEntry is using the same unique ID
+ entity_entry = ent_reg.async_get(SENSOR_NAME)
+ assert entity_entry.unique_id == old_unique_id
+
+
+async def test_skip_old_entity_migration_for_multiple(
+ hass, hank_binary_switch_state, client, integration
+):
+ """Test that multiple entities of the same value but on a different endpoint get skipped."""
+ node = Node(client, hank_binary_switch_state)
+
+ ent_reg = er.async_get(hass)
+ dev_reg = dr.async_get(hass)
+ device = dev_reg.async_get_or_create(
+ config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)}
+ )
+
+ SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
+ entity_name = SENSOR_NAME.split(".")[1]
+
+ # Create two entity entrrys using different endpoints
+ old_unique_id_1 = f"{client.driver.controller.home_id}.32-50-1-value-66049"
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id_1,
+ suggested_object_id=f"{entity_name}_1",
+ config_entry=integration,
+ original_name=f"{entity_name}_1",
+ device_id=device.id,
+ )
+ assert entity_entry.entity_id == f"{SENSOR_NAME}_1"
+ assert entity_entry.unique_id == old_unique_id_1
+
+ # Create two entity entrrys using different endpoints
+ old_unique_id_2 = f"{client.driver.controller.home_id}.32-50-2-value-66049"
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id_2,
+ suggested_object_id=f"{entity_name}_2",
+ config_entry=integration,
+ original_name=f"{entity_name}_2",
+ device_id=device.id,
+ )
+ assert entity_entry.entity_id == f"{SENSOR_NAME}_2"
+ assert entity_entry.unique_id == old_unique_id_2
+ # Add a ready node, unique ID should be migrated
+ event = {"node": node}
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is created using new unique ID format
+ entity_entry = ent_reg.async_get(SENSOR_NAME)
+ new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
+ assert entity_entry.unique_id == new_unique_id
+
+ # Check that the old entities stuck around because we skipped the migration step
+ assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1)
+ assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_2)
+
+
+async def test_old_entity_migration_notification_binary_sensor(
+ hass, multisensor_6_state, client, integration
+):
+ """Test old entity on a different endpoint is migrated to a new one for a notification binary sensor."""
+ node = Node(client, multisensor_6_state)
+
+ ent_reg = er.async_get(hass)
+ dev_reg = dr.async_get(hass)
+ device = dev_reg.async_get_or_create(
+ config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)}
+ )
+
+ entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id = f"{client.driver.controller.home_id}.52-113-1-Home Security-Motion sensor status.8"
+ entity_entry = ent_reg.async_get_or_create(
+ "binary_sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ device_id=device.id,
+ )
+ assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR
+ assert entity_entry.unique_id == old_unique_id
+
+ # Do this twice to make sure re-interview doesn't do anything weird
+ for _ in range(0, 2):
+ # Add a ready node, unique ID should be migrated
+ event = {"node": node}
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR)
+ new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8"
+ assert entity_entry.unique_id == new_unique_id
+ assert (
+ ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None
+ )
diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py
index afd3ae1a984..fc6d274235d 100644
--- a/tests/components/zwave_js/test_sensor.py
+++ b/tests/components/zwave_js/test_sensor.py
@@ -1,4 +1,6 @@
"""Test the Z-Wave JS sensor platform."""
+from zwave_js_server.event import Event
+
from homeassistant.const import (
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_HUMIDITY,
@@ -85,3 +87,47 @@ async def test_config_parameter_sensor(hass, lock_id_lock_as_id150, integration)
entity_entry = ent_reg.async_get(ID_LOCK_CONFIG_PARAMETER_SENSOR)
assert entity_entry
assert entity_entry.disabled
+
+
+async def test_node_status_sensor(hass, lock_id_lock_as_id150, integration):
+ """Test node status sensor is created and gets updated on node state changes."""
+ NODE_STATUS_ENTITY = "sensor.z_wave_module_for_id_lock_150_and_101_node_status"
+ node = lock_id_lock_as_id150
+ ent_reg = er.async_get(hass)
+ entity_entry = ent_reg.async_get(NODE_STATUS_ENTITY)
+ assert entity_entry.disabled
+ assert entity_entry.disabled_by == er.DISABLED_INTEGRATION
+ updated_entry = ent_reg.async_update_entity(
+ entity_entry.entity_id, **{"disabled_by": None}
+ )
+
+ await hass.config_entries.async_reload(integration.entry_id)
+ await hass.async_block_till_done()
+
+ assert not updated_entry.disabled
+ assert hass.states.get(NODE_STATUS_ENTITY).state == "alive"
+
+ # Test transitions work
+ event = Event(
+ "dead", data={"source": "node", "event": "dead", "nodeId": node.node_id}
+ )
+ node.receive_event(event)
+ assert hass.states.get(NODE_STATUS_ENTITY).state == "dead"
+
+ event = Event(
+ "wake up", data={"source": "node", "event": "wake up", "nodeId": node.node_id}
+ )
+ node.receive_event(event)
+ assert hass.states.get(NODE_STATUS_ENTITY).state == "awake"
+
+ event = Event(
+ "sleep", data={"source": "node", "event": "sleep", "nodeId": node.node_id}
+ )
+ node.receive_event(event)
+ assert hass.states.get(NODE_STATUS_ENTITY).state == "asleep"
+
+ event = Event(
+ "alive", data={"source": "node", "event": "alive", "nodeId": node.node_id}
+ )
+ node.receive_event(event)
+ assert hass.states.get(NODE_STATUS_ENTITY).state == "alive"
diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py
index 3c08c49a36f..dfc7ddaa85d 100644
--- a/tests/components/zwave_js/test_services.py
+++ b/tests/components/zwave_js/test_services.py
@@ -1,9 +1,12 @@
"""Test the Z-Wave JS services."""
+from unittest.mock import MagicMock, patch
+
import pytest
import voluptuous as vol
from zwave_js_server.exceptions import SetValueFailed
from homeassistant.components.zwave_js.const import (
+ ATTR_BROADCAST,
ATTR_COMMAND_CLASS,
ATTR_CONFIG_PARAMETER,
ATTR_CONFIG_PARAMETER_BITMASK,
@@ -14,6 +17,8 @@ from homeassistant.components.zwave_js.const import (
ATTR_WAIT_FOR_RESULT,
DOMAIN,
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
+ SERVICE_MULTICAST_SET_VALUE,
+ SERVICE_PING,
SERVICE_REFRESH_VALUE,
SERVICE_SET_CONFIG_PARAMETER,
SERVICE_SET_VALUE,
@@ -84,6 +89,50 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration):
client.async_send_command_no_wait.reset_mock()
+ # Test setting config parameter value in hex
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_CONFIG_PARAMETER,
+ {
+ ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
+ ATTR_CONFIG_PARAMETER: 102,
+ ATTR_CONFIG_PARAMETER_BITMASK: 1,
+ ATTR_CONFIG_VALUE: "0x1",
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClassName": "Configuration",
+ "commandClass": 112,
+ "endpoint": 0,
+ "property": 102,
+ "propertyName": "Group 2: Send battery reports",
+ "propertyKey": 1,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "valueSize": 4,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": True,
+ "label": "Group 2: Send battery reports",
+ "description": "Include battery information in periodic reports to Group 2",
+ "isFromConfig": True,
+ },
+ "value": 0,
+ }
+ assert args["value"] == 1
+
+ client.async_send_command_no_wait.reset_mock()
+
# Test setting parameter by property name
await hass.services.async_call(
DOMAIN,
@@ -212,8 +261,8 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration):
}
assert args["value"] == 1
- # Test that an invalid entity ID raises a ValueError
- with pytest.raises(ValueError):
+ # Test that an invalid entity ID raises a MultipleInvalid
+ with pytest.raises(vol.MultipleInvalid):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_CONFIG_PARAMETER,
@@ -225,8 +274,8 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration):
blocking=True,
)
- # Test that an invalid device ID raises a ValueError
- with pytest.raises(ValueError):
+ # Test that an invalid device ID raises a MultipleInvalid
+ with pytest.raises(vol.MultipleInvalid):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_CONFIG_PARAMETER,
@@ -259,8 +308,8 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration):
identifiers={("test", "test")},
)
- # Test that a non Z-Wave JS device raises a ValueError
- with pytest.raises(ValueError):
+ # Test that a non Z-Wave JS device raises a MultipleInvalid
+ with pytest.raises(vol.MultipleInvalid):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_CONFIG_PARAMETER,
@@ -276,8 +325,8 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration):
config_entry_id=integration.entry_id, identifiers={(DOMAIN, "500-500")}
)
- # Test that a Z-Wave JS device with an invalid node ID raises a ValueError
- with pytest.raises(ValueError):
+ # Test that a Z-Wave JS device with an invalid node ID raises a MultipleInvalid
+ with pytest.raises(vol.MultipleInvalid):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_CONFIG_PARAMETER,
@@ -297,8 +346,8 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration):
config_entry=non_zwave_js_config_entry,
)
- # Test that a non Z-Wave JS entity raises a ValueError
- with pytest.raises(ValueError):
+ # Test that a non Z-Wave JS entity raises a MultipleInvalid
+ with pytest.raises(vol.MultipleInvalid):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_CONFIG_PARAMETER,
@@ -414,6 +463,36 @@ async def test_bulk_set_config_parameters(hass, client, multisensor_6, integrati
client.async_send_command_no_wait.reset_mock()
+ # Test using hex values for config parameter values
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
+ {
+ ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
+ ATTR_CONFIG_PARAMETER: 102,
+ ATTR_CONFIG_VALUE: {
+ 1: "0x1",
+ 16: "0x1",
+ 32: "0x1",
+ 64: "0x1",
+ 128: "0x1",
+ },
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClass": 112,
+ "property": 102,
+ }
+ assert args["value"] == 241
+
+ client.async_send_command_no_wait.reset_mock()
+
await hass.services.async_call(
DOMAIN,
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
@@ -530,8 +609,23 @@ async def test_poll_value(
)
assert len(client.async_send_command.call_args_list) == 8
- # Test polling against an invalid entity raises ValueError
- with pytest.raises(ValueError):
+ client.async_send_command.reset_mock()
+
+ # Test polling all watched values using string for boolean
+ client.async_send_command.return_value = {"result": 2}
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_REFRESH_VALUE,
+ {
+ ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
+ ATTR_REFRESH_ALL_VALUES: "true",
+ },
+ blocking=True,
+ )
+ assert len(client.async_send_command.call_args_list) == 8
+
+ # Test polling against an invalid entity raises MultipleInvalid
+ with pytest.raises(vol.MultipleInvalid):
await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_VALUE,
@@ -581,6 +675,44 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration):
client.async_send_command_no_wait.reset_mock()
+ # Test bitmask as value and non bool as bool
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_VALUE,
+ {
+ ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY,
+ ATTR_COMMAND_CLASS: 117,
+ ATTR_PROPERTY: "local",
+ ATTR_VALUE: "0x2",
+ ATTR_WAIT_FOR_RESULT: 1,
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 5
+ assert args["valueId"] == {
+ "commandClassName": "Protection",
+ "commandClass": 117,
+ "endpoint": 0,
+ "property": "local",
+ "propertyName": "local",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "label": "Local protection state",
+ "states": {"0": "Unprotected", "2": "NoOperationPossible"},
+ },
+ "value": 0,
+ }
+ assert args["value"] == 2
+
+ client.async_send_command.reset_mock()
+
# Test that when a command fails we raise an exception
client.async_send_command.return_value = {"success": False}
@@ -634,3 +766,226 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration):
},
blocking=True,
)
+
+
+async def test_multicast_set_value(
+ hass,
+ client,
+ climate_danfoss_lc_13,
+ climate_radio_thermostat_ct100_plus_different_endpoints,
+ integration,
+):
+ """Test multicast_set_value service."""
+ # Test successful multicast call
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_MULTICAST_SET_VALUE,
+ {
+ ATTR_ENTITY_ID: [
+ CLIMATE_DANFOSS_LC13_ENTITY,
+ CLIMATE_RADIO_THERMOSTAT_ENTITY,
+ ],
+ ATTR_COMMAND_CLASS: 117,
+ ATTR_PROPERTY: "local",
+ ATTR_VALUE: 2,
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "multicast_group.set_value"
+ assert args["nodeIDs"] == [
+ climate_radio_thermostat_ct100_plus_different_endpoints.node_id,
+ climate_danfoss_lc_13.node_id,
+ ]
+ assert args["valueId"] == {
+ "commandClass": 117,
+ "property": "local",
+ }
+ assert args["value"] == 2
+
+ client.async_send_command.reset_mock()
+
+ # Test successful multicast call with hex value
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_MULTICAST_SET_VALUE,
+ {
+ ATTR_ENTITY_ID: [
+ CLIMATE_DANFOSS_LC13_ENTITY,
+ CLIMATE_RADIO_THERMOSTAT_ENTITY,
+ ],
+ ATTR_COMMAND_CLASS: 117,
+ ATTR_PROPERTY: "local",
+ ATTR_VALUE: "0x2",
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "multicast_group.set_value"
+ assert args["nodeIDs"] == [
+ climate_radio_thermostat_ct100_plus_different_endpoints.node_id,
+ climate_danfoss_lc_13.node_id,
+ ]
+ assert args["valueId"] == {
+ "commandClass": 117,
+ "property": "local",
+ }
+ assert args["value"] == 2
+
+ client.async_send_command.reset_mock()
+
+ # Test successful broadcast call
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_MULTICAST_SET_VALUE,
+ {
+ ATTR_BROADCAST: True,
+ ATTR_COMMAND_CLASS: 117,
+ ATTR_PROPERTY: "local",
+ ATTR_VALUE: 2,
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "broadcast_node.set_value"
+ assert args["valueId"] == {
+ "commandClass": 117,
+ "property": "local",
+ }
+ assert args["value"] == 2
+
+ client.async_send_command.reset_mock()
+
+ # Test sending one node without broadcast fails
+ with pytest.raises(vol.Invalid):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_MULTICAST_SET_VALUE,
+ {
+ ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY,
+ ATTR_COMMAND_CLASS: 117,
+ ATTR_PROPERTY: "local",
+ ATTR_VALUE: 2,
+ },
+ blocking=True,
+ )
+
+ # Test no device, entity, or broadcast flag raises error
+ with pytest.raises(vol.Invalid):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_MULTICAST_SET_VALUE,
+ {
+ ATTR_COMMAND_CLASS: 117,
+ ATTR_PROPERTY: "local",
+ ATTR_VALUE: 2,
+ },
+ blocking=True,
+ )
+
+ # Test that when a command fails we raise an exception
+ client.async_send_command.return_value = {"success": False}
+
+ with pytest.raises(SetValueFailed):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_MULTICAST_SET_VALUE,
+ {
+ ATTR_ENTITY_ID: [
+ CLIMATE_DANFOSS_LC13_ENTITY,
+ CLIMATE_RADIO_THERMOSTAT_ENTITY,
+ ],
+ ATTR_COMMAND_CLASS: 117,
+ ATTR_PROPERTY: "local",
+ ATTR_VALUE: 2,
+ },
+ blocking=True,
+ )
+
+ # Create a fake node with a different home ID from a real node and patch it into
+ # return of helper function to check the validation for two nodes having different
+ # home IDs
+ diff_network_node = MagicMock()
+ diff_network_node.client.driver.controller.home_id.return_value = "diff_home_id"
+
+ with pytest.raises(vol.MultipleInvalid), patch(
+ "homeassistant.components.zwave_js.services.async_get_node_from_device_id",
+ return_value=diff_network_node,
+ ):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_MULTICAST_SET_VALUE,
+ {
+ ATTR_ENTITY_ID: [
+ CLIMATE_DANFOSS_LC13_ENTITY,
+ ],
+ ATTR_DEVICE_ID: "fake_device_id",
+ ATTR_COMMAND_CLASS: 117,
+ ATTR_PROPERTY: "local",
+ ATTR_VALUE: 2,
+ },
+ blocking=True,
+ )
+
+ # Test that when there are multiple zwave_js config entries, service will fail
+ # without devices or entities
+ new_entry = MockConfigEntry(domain=DOMAIN)
+ new_entry.add_to_hass(hass)
+ with pytest.raises(vol.Invalid):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_MULTICAST_SET_VALUE,
+ {
+ ATTR_BROADCAST: True,
+ ATTR_COMMAND_CLASS: 117,
+ ATTR_PROPERTY: "local",
+ ATTR_VALUE: 2,
+ },
+ blocking=True,
+ )
+
+
+async def test_ping(
+ hass,
+ client,
+ climate_danfoss_lc_13,
+ climate_radio_thermostat_ct100_plus_different_endpoints,
+ integration,
+):
+ """Test ping service."""
+ client.async_send_command.return_value = {"responded": True}
+
+ # Test successful ping call
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_PING,
+ {
+ ATTR_ENTITY_ID: [
+ CLIMATE_DANFOSS_LC13_ENTITY,
+ CLIMATE_RADIO_THERMOSTAT_ENTITY,
+ ],
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 2
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.ping"
+ assert args["nodeId"] == climate_danfoss_lc_13.node_id
+
+ client.async_send_command.reset_mock()
+
+ # Test no device or entity raises error
+ with pytest.raises(vol.Invalid):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_PING,
+ {},
+ blocking=True,
+ )
diff --git a/tests/fixtures/advantage_air/getSystemData.json b/tests/fixtures/advantage_air/getSystemData.json
index 19dda28fec1..4ed610f9649 100644
--- a/tests/fixtures/advantage_air/getSystemData.json
+++ b/tests/fixtures/advantage_air/getSystemData.json
@@ -100,7 +100,10 @@
"fan": "low",
"filterCleanStatus": 1,
"freshAirStatus": "none",
- "mode": "cool",
+ "mode": "myauto",
+ "myAutoModeCurrentSetMode": "cool",
+ "myAutoModeEnabled": true,
+ "myAutoModeIsRunning": true,
"myZone": 1,
"name": "AC Two",
"setTemp": 24,
diff --git a/tests/fixtures/ambee/air_quality.json b/tests/fixtures/ambee/air_quality.json
new file mode 100644
index 00000000000..2844e38168b
--- /dev/null
+++ b/tests/fixtures/ambee/air_quality.json
@@ -0,0 +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"
+ }
+ }
+ ]
+}
diff --git a/tests/fixtures/ambee/pollen.json b/tests/fixtures/ambee/pollen.json
new file mode 100644
index 00000000000..95f8a96c3c8
--- /dev/null
+++ b/tests/fixtures/ambee/pollen.json
@@ -0,0 +1,43 @@
+{
+ "message": "Success",
+ "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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/climacell/v4.json b/tests/fixtures/climacell/v4.json
index f2f10b0360e..02f76ab7d27 100644
--- a/tests/fixtures/climacell/v4.json
+++ b/tests/fixtures/climacell/v4.json
@@ -25,7 +25,13 @@
"treeIndex": 0,
"weedIndex": 0,
"grassIndex": 0,
- "fireIndex": 10
+ "fireIndex": 10,
+ "temperatureApparent": 101.3,
+ "dewPoint": 72.82,
+ "pressureSurfaceLevel": 29.47,
+ "solarGHI": 0,
+ "cloudBase": 0.74,
+ "cloudCeiling": 0.74
},
"forecasts": {
"nowcast": [
diff --git a/tests/fixtures/ecobee/ecobee-data.json b/tests/fixtures/ecobee/ecobee-data.json
index 2727103c9b1..a4caa72798d 100644
--- a/tests/fixtures/ecobee/ecobee-data.json
+++ b/tests/fixtures/ecobee/ecobee-data.json
@@ -1,6 +1,9 @@
{
"thermostatList": [
- {"name": "ecobee",
+ {
+ "identifier": 8675309,
+ "name": "ecobee",
+ "modelNumber": "athenaSmart",
"program": {
"climates": [
{"name": "Climate1", "climateRef": "c1"},
@@ -9,6 +12,7 @@
"currentClimateRef": "c1"
},
"runtime": {
+ "connected": true,
"actualTemperature": 300,
"actualHumidity": 15,
"desiredHeat": 400,
@@ -24,7 +28,7 @@
"heatCoolMinDelta": 50,
"holdAction": "nextTransition",
"hasHumidifier": true,
- "humidifierMode": "off",
+ "humidifierMode": "manual",
"humidity": "30"
},
"equipmentStatus": "fan",
@@ -37,7 +41,28 @@
"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"
+ }
+ ]
+ }
+ ]
+ }
]
+}
-}
\ No newline at end of file
diff --git a/tests/fixtures/lcn/config.json b/tests/fixtures/lcn/config.json
new file mode 100644
index 00000000000..50a1ca05e29
--- /dev/null
+++ b/tests/fixtures/lcn/config.json
@@ -0,0 +1,31 @@
+{
+ "lcn": {
+ "connections": [
+ {
+ "host": "192.168.2.41",
+ "port": 4114,
+ "username": "lcn",
+ "password": "lcn",
+ "sk_num_tries": 0,
+ "dim_mode": "steps200",
+ "name": "pchk"
+ },
+ {
+ "name": "myhome",
+ "host": "192.168.2.42",
+ "port": 4114,
+ "username": "lcn",
+ "password": "lcn",
+ "sk_num_tries": 0,
+ "dim_mode": "steps200"
+ }
+ ],
+ "switches": [
+ {
+ "name": "Switch_Output1",
+ "address": "s0.m7",
+ "output": "output1"
+ }
+ ]
+ }
+}
diff --git a/tests/fixtures/lcn/config_entry_myhome.json b/tests/fixtures/lcn/config_entry_myhome.json
new file mode 100644
index 00000000000..8ab59d0087d
--- /dev/null
+++ b/tests/fixtures/lcn/config_entry_myhome.json
@@ -0,0 +1,11 @@
+{
+ "host": "myhome",
+ "ip_address": "192.168.2.42",
+ "port": 4114,
+ "username": "lcn",
+ "password": "lcn",
+ "sk_num_tries": 0,
+ "dim_mode": "STEPS200",
+ "devices": [],
+ "entities": []
+}
diff --git a/tests/fixtures/lcn/config_entry_pchk.json b/tests/fixtures/lcn/config_entry_pchk.json
new file mode 100644
index 00000000000..3058389a95d
--- /dev/null
+++ b/tests/fixtures/lcn/config_entry_pchk.json
@@ -0,0 +1,29 @@
+{
+ "host": "pchk",
+ "ip_address": "192.168.2.41",
+ "port": 4114,
+ "username": "lcn",
+ "password": "lcn",
+ "sk_num_tries": 0,
+ "dim_mode": "STEPS200",
+ "devices": [
+ {
+ "address": [0, 7, false],
+ "name": "",
+ "hardware_serial": -1,
+ "software_serial": -1,
+ "hardware_type": -1
+ }
+ ],
+ "entities": [
+ {
+ "address": [0, 7, false],
+ "name": "Switch_Output1",
+ "resource": "output1",
+ "domain": "switch",
+ "domain_data": {
+ "output": "OUTPUT1"
+ }
+ }
+ ]
+}
diff --git a/tests/fixtures/metoffice.json b/tests/fixtures/metoffice.json
index c2b8707ca7a..22a0673c4dd 100644
--- a/tests/fixtures/metoffice.json
+++ b/tests/fixtures/metoffice.json
@@ -218,6 +218,7 @@
"U": "0",
"$": "180"
},
+
{
"D": "NW",
"F": "10",
@@ -1495,5 +1496,258 @@
}
}
}
+ },
+ "kingslynn_daily": {
+ "SiteRep": {
+ "Wx": {
+ "Param": [
+ {
+ "name": "FDm",
+ "units": "C",
+ "$": "Feels Like Day Maximum Temperature"
+ },
+ {
+ "name": "FNm",
+ "units": "C",
+ "$": "Feels Like Night Minimum Temperature"
+ },
+ {
+ "name": "Dm",
+ "units": "C",
+ "$": "Day Maximum Temperature"
+ },
+ {
+ "name": "Nm",
+ "units": "C",
+ "$": "Night Minimum Temperature"
+ },
+ {
+ "name": "Gn",
+ "units": "mph",
+ "$": "Wind Gust Noon"
+ },
+ {
+ "name": "Gm",
+ "units": "mph",
+ "$": "Wind Gust Midnight"
+ },
+ {
+ "name": "Hn",
+ "units": "%",
+ "$": "Screen Relative Humidity Noon"
+ },
+ {
+ "name": "Hm",
+ "units": "%",
+ "$": "Screen Relative Humidity Midnight"
+ },
+ {
+ "name": "V",
+ "units": "",
+ "$": "Visibility"
+ },
+ {
+ "name": "D",
+ "units": "compass",
+ "$": "Wind Direction"
+ },
+ {
+ "name": "S",
+ "units": "mph",
+ "$": "Wind Speed"
+ },
+ {
+ "name": "U",
+ "units": "",
+ "$": "Max UV Index"
+ },
+ {
+ "name": "W",
+ "units": "",
+ "$": "Weather Type"
+ },
+ {
+ "name": "PPd",
+ "units": "%",
+ "$": "Precipitation Probability Day"
+ },
+ {
+ "name": "PPn",
+ "units": "%",
+ "$": "Precipitation Probability Night"
+ }
+ ]
+ },
+ "DV": {
+ "dataDate": "2020-04-25T08:00:00Z",
+ "type": "Forecast",
+ "Location": {
+ "i": "322380",
+ "lat": "52.7561",
+ "lon": "0.4019",
+ "name": "KING'S LYNN",
+ "country": "ENGLAND",
+ "continent": "EUROPE",
+ "elevation": "5.0",
+ "Period": [
+ {
+ "type": "Day",
+ "value": "2020-04-25Z",
+ "Rep": [
+ {
+ "D": "ESE",
+ "Gn": "4",
+ "Hn": "75",
+ "PPd": "9",
+ "S": "4",
+ "V": "VG",
+ "Dm": "9",
+ "FDm": "8",
+ "W": "8",
+ "U": "3",
+ "$": "Day"
+ },
+ {
+ "D": "SSE",
+ "Gm": "16",
+ "Hm": "84",
+ "PPn": "0",
+ "S": "7",
+ "V": "VG",
+ "Nm": "7",
+ "FNm": "5",
+ "W": "0",
+ "$": "Night"
+ }
+ ]
+ },
+ {
+ "type": "Day",
+ "value": "2020-04-26Z",
+ "Rep": [
+ {
+ "D": "SSW",
+ "Gn": "13",
+ "Hn": "69",
+ "PPd": "0",
+ "S": "9",
+ "V": "VG",
+ "Dm": "13",
+ "FDm": "11",
+ "W": "1",
+ "U": "4",
+ "$": "Day"
+ },
+ {
+ "D": "SSW",
+ "Gm": "13",
+ "Hm": "75",
+ "PPn": "5",
+ "S": "7",
+ "V": "GO",
+ "Nm": "11",
+ "FNm": "10",
+ "W": "7",
+ "$": "Night"
+ }
+ ]
+ },
+ {
+ "type": "Day",
+ "value": "2020-04-27Z",
+ "Rep": [
+ {
+ "D": "NW",
+ "Gn": "11",
+ "Hn": "78",
+ "PPd": "36",
+ "S": "4",
+ "V": "VG",
+ "Dm": "10",
+ "FDm": "9",
+ "W": "7",
+ "U": "3",
+ "$": "Day"
+ },
+ {
+ "D": "SE",
+ "Gm": "13",
+ "Hm": "85",
+ "PPn": "9",
+ "S": "7",
+ "V": "VG",
+ "Nm": "9",
+ "FNm": "7",
+ "W": "7",
+ "$": "Night"
+ }
+ ]
+ },
+ {
+ "type": "Day",
+ "value": "2020-04-28Z",
+ "Rep": [
+ {
+ "D": "ESE",
+ "Gn": "13",
+ "Hn": "77",
+ "PPd": "14",
+ "S": "7",
+ "V": "GO",
+ "Dm": "11",
+ "FDm": "9",
+ "W": "7",
+ "U": "3",
+ "$": "Day"
+ },
+ {
+ "D": "SSE",
+ "Gm": "13",
+ "Hm": "87",
+ "PPn": "11",
+ "S": "7",
+ "V": "GO",
+ "Nm": "9",
+ "FNm": "7",
+ "W": "7",
+ "$": "Night"
+ }
+ ]
+ },
+ {
+ "type": "Day",
+ "value": "2020-04-29Z",
+ "Rep": [
+ {
+ "D": "SSE",
+ "Gn": "20",
+ "Hn": "75",
+ "PPd": "8",
+ "S": "11",
+ "V": "VG",
+ "Dm": "12",
+ "FDm": "10",
+ "W": "7",
+ "U": "3",
+ "$": "Day"
+ },
+ {
+ "D": "SSE",
+ "Gm": "20",
+ "Hm": "86",
+ "PPn": "20",
+ "S": "11",
+ "V": "VG",
+ "Nm": "9",
+ "FNm": "7",
+ "W": "7",
+ "$": "Night"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/tests/fixtures/modern_forms/device_info.json b/tests/fixtures/modern_forms/device_info.json
new file mode 100644
index 00000000000..e63f79fd468
--- /dev/null
+++ b/tests/fixtures/modern_forms/device_info.json
@@ -0,0 +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": ""
+}
diff --git a/tests/fixtures/modern_forms/device_info_no_light.json b/tests/fixtures/modern_forms/device_info_no_light.json
new file mode 100644
index 00000000000..5557af57531
--- /dev/null
+++ b/tests/fixtures/modern_forms/device_info_no_light.json
@@ -0,0 +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": ""
+}
diff --git a/tests/fixtures/modern_forms/device_status.json b/tests/fixtures/modern_forms/device_status.json
new file mode 100644
index 00000000000..c982f884375
--- /dev/null
+++ b/tests/fixtures/modern_forms/device_status.json
@@ -0,0 +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": ""
+}
diff --git a/tests/fixtures/modern_forms/device_status_no_light.json b/tests/fixtures/modern_forms/device_status_no_light.json
new file mode 100644
index 00000000000..ca499b271fb
--- /dev/null
+++ b/tests/fixtures/modern_forms/device_status_no_light.json
@@ -0,0 +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": ""
+}
diff --git a/tests/fixtures/modern_forms/device_status_timers_active.json b/tests/fixtures/modern_forms/device_status_timers_active.json
new file mode 100644
index 00000000000..e788b3e5882
--- /dev/null
+++ b/tests/fixtures/modern_forms/device_status_timers_active.json
@@ -0,0 +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": ""
+}
diff --git a/tests/fixtures/mysensors/gps_sensor_state.json b/tests/fixtures/mysensors/gps_sensor_state.json
new file mode 100644
index 00000000000..654e30e7271
--- /dev/null
+++ b/tests/fixtures/mysensors/gps_sensor_state.json
@@ -0,0 +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
+ }
+}
diff --git a/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2021_06_01.json b/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2021_06_01.json
new file mode 100644
index 00000000000..59559d3c3f7
--- /dev/null
+++ b/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2021_06_01.json
@@ -0,0 +1,604 @@
+{
+ "PVPC": [
+ {
+ "Dia": "01/06/2021",
+ "Hora": "00-01",
+ "PCB": "116,33",
+ "CYM": "116,33",
+ "COF2TD": "0,000088075182000000",
+ "PMHPCB": "104,00",
+ "PMHCYM": "104,00",
+ "SAHPCB": "3,56",
+ "SAHCYM": "3,56",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,00",
+ "PCAPCYM": "0,00",
+ "TEUPCB": "6,00",
+ "TEUCYM": "6,00",
+ "CCVPCB": "2,57",
+ "CCVCYM": "2,57",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "01-02",
+ "PCB": "115,95",
+ "CYM": "115,95",
+ "COF2TD": "0,000073094842000000",
+ "PMHPCB": "103,18",
+ "PMHCYM": "103,18",
+ "SAHPCB": "3,99",
+ "SAHCYM": "3,99",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,00",
+ "PCAPCYM": "0,00",
+ "TEUPCB": "6,00",
+ "TEUCYM": "6,00",
+ "CCVPCB": "2,58",
+ "CCVCYM": "2,58",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "02-03",
+ "PCB": "114,89",
+ "CYM": "114,89",
+ "COF2TD": "0,000065114032000000",
+ "PMHPCB": "101,87",
+ "PMHCYM": "101,87",
+ "SAHPCB": "4,25",
+ "SAHCYM": "4,25",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,00",
+ "PCAPCYM": "0,00",
+ "TEUPCB": "6,00",
+ "TEUCYM": "6,00",
+ "CCVPCB": "2,56",
+ "CCVCYM": "2,56",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "03-04",
+ "PCB": "114,96",
+ "CYM": "114,96",
+ "COF2TD": "0,000061272596000000",
+ "PMHPCB": "102,01",
+ "PMHCYM": "102,01",
+ "SAHPCB": "4,19",
+ "SAHCYM": "4,19",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,00",
+ "PCAPCYM": "0,00",
+ "TEUPCB": "6,00",
+ "TEUCYM": "6,00",
+ "CCVPCB": "2,57",
+ "CCVCYM": "2,57",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "04-05",
+ "PCB": "114,84",
+ "CYM": "114,84",
+ "COF2TD": "0,000059563056000000",
+ "PMHPCB": "101,87",
+ "PMHCYM": "101,87",
+ "SAHPCB": "4,21",
+ "SAHCYM": "4,21",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,00",
+ "PCAPCYM": "0,00",
+ "TEUPCB": "6,00",
+ "TEUCYM": "6,00",
+ "CCVPCB": "2,56",
+ "CCVCYM": "2,56",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "05-06",
+ "PCB": "116,03",
+ "CYM": "116,03",
+ "COF2TD": "0,000059907686000000",
+ "PMHPCB": "103,14",
+ "PMHCYM": "103,14",
+ "SAHPCB": "4,11",
+ "SAHCYM": "4,11",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,00",
+ "PCAPCYM": "0,00",
+ "TEUPCB": "6,00",
+ "TEUCYM": "6,00",
+ "CCVPCB": "2,58",
+ "CCVCYM": "2,58",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "06-07",
+ "PCB": "116,29",
+ "CYM": "116,29",
+ "COF2TD": "0,000062818713000000",
+ "PMHPCB": "103,64",
+ "PMHCYM": "103,64",
+ "SAHPCB": "3,88",
+ "SAHCYM": "3,88",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,00",
+ "PCAPCYM": "0,00",
+ "TEUPCB": "6,00",
+ "TEUCYM": "6,00",
+ "CCVPCB": "2,57",
+ "CCVCYM": "2,57",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "07-08",
+ "PCB": "115,70",
+ "CYM": "115,70",
+ "COF2TD": "0,000072575564000000",
+ "PMHPCB": "103,85",
+ "PMHCYM": "103,85",
+ "SAHPCB": "3,10",
+ "SAHCYM": "3,10",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,16",
+ "FOSCYM": "0,16",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,00",
+ "PCAPCYM": "0,00",
+ "TEUPCB": "6,00",
+ "TEUCYM": "6,00",
+ "CCVPCB": "2,55",
+ "CCVCYM": "2,55",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "08-09",
+ "PCB": "152,89",
+ "CYM": "152,89",
+ "COF2TD": "0,000086825264000000",
+ "PMHPCB": "105,65",
+ "PMHCYM": "105,65",
+ "SAHPCB": "2,36",
+ "SAHCYM": "2,36",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,34",
+ "PCAPCYM": "0,34",
+ "TEUPCB": "41,77",
+ "TEUCYM": "41,77",
+ "CCVPCB": "2,57",
+ "CCVCYM": "2,57",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "09-10",
+ "PCB": "150,83",
+ "CYM": "150,83",
+ "COF2TD": "0,000095768317000000",
+ "PMHPCB": "103,77",
+ "PMHCYM": "103,77",
+ "SAHPCB": "2,24",
+ "SAHCYM": "2,24",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,16",
+ "FOSCYM": "0,16",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,34",
+ "PCAPCYM": "0,34",
+ "TEUPCB": "41,77",
+ "TEUCYM": "41,77",
+ "CCVPCB": "2,53",
+ "CCVCYM": "2,53",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "10-11",
+ "PCB": "242,62",
+ "CYM": "149,28",
+ "COF2TD": "0,000102672431000000",
+ "PMHPCB": "102,38",
+ "PMHCYM": "102,11",
+ "SAHPCB": "2,38",
+ "SAHCYM": "2,37",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,16",
+ "FOSCYM": "0,16",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "2,01",
+ "PCAPCYM": "0,34",
+ "TEUPCB": "133,12",
+ "TEUCYM": "41,77",
+ "CCVPCB": "2,54",
+ "CCVCYM": "2,51",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "11-12",
+ "PCB": "240,50",
+ "CYM": "240,50",
+ "COF2TD": "0,000105691470000000",
+ "PMHPCB": "100,14",
+ "PMHCYM": "100,14",
+ "SAHPCB": "2,52",
+ "SAHCYM": "2,52",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,16",
+ "FOSCYM": "0,16",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "2,02",
+ "PCAPCYM": "2,02",
+ "TEUPCB": "133,12",
+ "TEUCYM": "133,12",
+ "CCVPCB": "2,51",
+ "CCVCYM": "2,51",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "12-13",
+ "PCB": "238,09",
+ "CYM": "238,09",
+ "COF2TD": "0,000110462952000000",
+ "PMHPCB": "97,58",
+ "PMHCYM": "97,58",
+ "SAHPCB": "2,71",
+ "SAHCYM": "2,71",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,16",
+ "FOSCYM": "0,16",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "2,02",
+ "PCAPCYM": "2,02",
+ "TEUPCB": "133,12",
+ "TEUCYM": "133,12",
+ "CCVPCB": "2,47",
+ "CCVCYM": "2,47",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "13-14",
+ "PCB": "235,30",
+ "CYM": "235,30",
+ "COF2TD": "0,000119052052000000",
+ "PMHPCB": "94,65",
+ "PMHCYM": "94,65",
+ "SAHPCB": "2,89",
+ "SAHCYM": "2,89",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,16",
+ "FOSCYM": "0,16",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "2,02",
+ "PCAPCYM": "2,02",
+ "TEUPCB": "133,12",
+ "TEUCYM": "133,12",
+ "CCVPCB": "2,43",
+ "CCVCYM": "2,43",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "14-15",
+ "PCB": "137,96",
+ "CYM": "231,28",
+ "COF2TD": "0,000117990009000000",
+ "PMHPCB": "89,95",
+ "PMHCYM": "90,19",
+ "SAHPCB": "3,37",
+ "SAHCYM": "3,38",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,16",
+ "FOSCYM": "0,16",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,34",
+ "PCAPCYM": "2,03",
+ "TEUPCB": "41,77",
+ "TEUCYM": "133,12",
+ "CCVPCB": "2,34",
+ "CCVCYM": "2,37",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "15-16",
+ "PCB": "132,88",
+ "CYM": "132,88",
+ "COF2TD": "0,000108598330000000",
+ "PMHPCB": "84,43",
+ "PMHCYM": "84,43",
+ "SAHPCB": "3,89",
+ "SAHCYM": "3,89",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,16",
+ "FOSCYM": "0,16",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,34",
+ "PCAPCYM": "0,34",
+ "TEUPCB": "41,77",
+ "TEUCYM": "41,77",
+ "CCVPCB": "2,26",
+ "CCVCYM": "2,26",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "16-17",
+ "PCB": "131,93",
+ "CYM": "131,93",
+ "COF2TD": "0,000104114191000000",
+ "PMHPCB": "83,66",
+ "PMHCYM": "83,66",
+ "SAHPCB": "3,73",
+ "SAHCYM": "3,73",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,16",
+ "FOSCYM": "0,16",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,34",
+ "PCAPCYM": "0,34",
+ "TEUPCB": "41,77",
+ "TEUCYM": "41,77",
+ "CCVPCB": "2,25",
+ "CCVCYM": "2,25",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "17-18",
+ "PCB": "135,99",
+ "CYM": "135,99",
+ "COF2TD": "0,000105171071000000",
+ "PMHPCB": "88,07",
+ "PMHCYM": "88,07",
+ "SAHPCB": "3,31",
+ "SAHCYM": "3,31",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,16",
+ "FOSCYM": "0,16",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,34",
+ "PCAPCYM": "0,34",
+ "TEUPCB": "41,77",
+ "TEUCYM": "41,77",
+ "CCVPCB": "2,31",
+ "CCVCYM": "2,31",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "18-19",
+ "PCB": "231,44",
+ "CYM": "138,13",
+ "COF2TD": "0,000106417649000000",
+ "PMHPCB": "90,57",
+ "PMHCYM": "90,33",
+ "SAHPCB": "3,16",
+ "SAHCYM": "3,15",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,16",
+ "FOSCYM": "0,16",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "2,02",
+ "PCAPCYM": "0,34",
+ "TEUPCB": "133,12",
+ "TEUCYM": "41,77",
+ "CCVPCB": "2,37",
+ "CCVCYM": "2,34",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "19-20",
+ "PCB": "240,40",
+ "CYM": "240,40",
+ "COF2TD": "0,000108017615000000",
+ "PMHPCB": "99,53",
+ "PMHCYM": "99,53",
+ "SAHPCB": "3,00",
+ "SAHCYM": "3,00",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "2,04",
+ "PCAPCYM": "2,04",
+ "TEUPCB": "133,12",
+ "TEUCYM": "133,12",
+ "CCVPCB": "2,52",
+ "CCVCYM": "2,52",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "20-21",
+ "PCB": "246,20",
+ "CYM": "246,20",
+ "COF2TD": "0,000114631042000000",
+ "PMHPCB": "104,32",
+ "PMHCYM": "104,32",
+ "SAHPCB": "3,90",
+ "SAHCYM": "3,90",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "2,05",
+ "PCAPCYM": "2,05",
+ "TEUPCB": "133,12",
+ "TEUCYM": "133,12",
+ "CCVPCB": "2,61",
+ "CCVCYM": "2,61",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "21-22",
+ "PCB": "248,08",
+ "CYM": "248,08",
+ "COF2TD": "0,000127585671000000",
+ "PMHPCB": "107,28",
+ "PMHCYM": "107,28",
+ "SAHPCB": "2,78",
+ "SAHCYM": "2,78",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "2,06",
+ "PCAPCYM": "2,06",
+ "TEUPCB": "133,12",
+ "TEUCYM": "133,12",
+ "CCVPCB": "2,64",
+ "CCVCYM": "2,64",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "22-23",
+ "PCB": "155,91",
+ "CYM": "249,41",
+ "COF2TD": "0,000130129026000000",
+ "PMHPCB": "108,02",
+ "PMHCYM": "108,39",
+ "SAHPCB": "2,93",
+ "SAHCYM": "2,94",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,35",
+ "PCAPCYM": "2,09",
+ "TEUPCB": "41,77",
+ "TEUCYM": "133,12",
+ "CCVPCB": "2,64",
+ "CCVCYM": "2,67",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ },
+ {
+ "Dia": "01/06/2021",
+ "Hora": "23-24",
+ "PCB": "156,50",
+ "CYM": "156,50",
+ "COF2TD": "0,000110367990000000",
+ "PMHPCB": "108,02",
+ "PMHCYM": "108,02",
+ "SAHPCB": "3,50",
+ "SAHCYM": "3,50",
+ "FOMPCB": "0,03",
+ "FOMCYM": "0,03",
+ "FOSPCB": "0,17",
+ "FOSCYM": "0,17",
+ "INTPCB": "0,00",
+ "INTCYM": "0,00",
+ "PCAPPCB": "0,35",
+ "PCAPCYM": "0,35",
+ "TEUPCB": "41,77",
+ "TEUCYM": "41,77",
+ "CCVPCB": "2,66",
+ "CCVCYM": "2,66",
+ "EDSRPCB": "0,00",
+ "EDSRCYM": "0,00"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/wled/rgb_websocket.json b/tests/fixtures/wled/rgb_websocket.json
new file mode 100644
index 00000000000..7e37b489549
--- /dev/null
+++ b/tests/fixtures/wled/rgb_websocket.json
@@ -0,0 +1,289 @@
+{
+ "state": {
+ "on": true,
+ "bri": 255,
+ "transition": 7,
+ "ps": -1,
+ "pl": -1,
+ "ccnf": {
+ "min": 1,
+ "max": 5,
+ "time": 12
+ },
+ "nl": {
+ "on": false,
+ "dur": 60,
+ "fade": true,
+ "mode": 1,
+ "tbri": 0,
+ "rem": -1
+ },
+ "udpn": {
+ "send": false,
+ "recv": true
+ },
+ "lor": 0,
+ "mainseg": 0,
+ "seg": [
+ {
+ "id": 0,
+ "start": 0,
+ "stop": 13,
+ "len": 13,
+ "grp": 1,
+ "spc": 0,
+ "on": true,
+ "bri": 255,
+ "col": [
+ [
+ 255,
+ 181,
+ 218
+ ],
+ [
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0
+ ]
+ ],
+ "fx": 0,
+ "sx": 43,
+ "ix": 128,
+ "pal": 2,
+ "sel": true,
+ "rev": false,
+ "mi": false
+ }
+ ]
+ },
+ "info": {
+ "ver": "0.12.0-b2",
+ "vid": 2103220,
+ "leds": {
+ "count": 13,
+ "rgbw": false,
+ "wv": false,
+ "pin": [
+ 2
+ ],
+ "pwr": 266,
+ "fps": 2,
+ "maxpwr": 1000,
+ "maxseg": 12,
+ "seglock": false
+ },
+ "str": false,
+ "name": "WLED WebSocket",
+ "udpport": 21324,
+ "live": false,
+ "lm": "",
+ "lip": "",
+ "ws": 0,
+ "fxcount": 118,
+ "palcount": 56,
+ "wifi": {
+ "bssid": "AA:AA:AA:AA:AA:BB",
+ "rssi": -68,
+ "signal": 64,
+ "channel": 6
+ },
+ "fs": {
+ "u": 40,
+ "t": 1024,
+ "pmt": 1623156685
+ },
+ "ndc": 1,
+ "arch": "esp8266",
+ "core": "2_7_4_7",
+ "lwip": 1,
+ "freeheap": 22752,
+ "uptime": 258411,
+ "opt": 127,
+ "brand": "WLED",
+ "product": "FOSS",
+ "mac": "aabbccddeeff"
+ },
+ "effects": [
+ "Solid",
+ "Blink",
+ "Breathe",
+ "Wipe",
+ "Wipe Random",
+ "Random Colors",
+ "Sweep",
+ "Dynamic",
+ "Colorloop",
+ "Rainbow",
+ "Scan",
+ "Scan Dual",
+ "Fade",
+ "Theater",
+ "Theater Rainbow",
+ "Running",
+ "Saw",
+ "Twinkle",
+ "Dissolve",
+ "Dissolve Rnd",
+ "Sparkle",
+ "Sparkle Dark",
+ "Sparkle+",
+ "Strobe",
+ "Strobe Rainbow",
+ "Strobe Mega",
+ "Blink Rainbow",
+ "Android",
+ "Chase",
+ "Chase Random",
+ "Chase Rainbow",
+ "Chase Flash",
+ "Chase Flash Rnd",
+ "Rainbow Runner",
+ "Colorful",
+ "Traffic Light",
+ "Sweep Random",
+ "Running 2",
+ "Aurora",
+ "Stream",
+ "Scanner",
+ "Lighthouse",
+ "Fireworks",
+ "Rain",
+ "Tetrix",
+ "Fire Flicker",
+ "Gradient",
+ "Loading",
+ "Police",
+ "Police All",
+ "Two Dots",
+ "Two Areas",
+ "Circus",
+ "Halloween",
+ "Tri Chase",
+ "Tri Wipe",
+ "Tri Fade",
+ "Lightning",
+ "ICU",
+ "Multi Comet",
+ "Scanner Dual",
+ "Stream 2",
+ "Oscillate",
+ "Pride 2015",
+ "Juggle",
+ "Palette",
+ "Fire 2012",
+ "Colorwaves",
+ "Bpm",
+ "Fill Noise",
+ "Noise 1",
+ "Noise 2",
+ "Noise 3",
+ "Noise 4",
+ "Colortwinkles",
+ "Lake",
+ "Meteor",
+ "Meteor Smooth",
+ "Railway",
+ "Ripple",
+ "Twinklefox",
+ "Twinklecat",
+ "Halloween Eyes",
+ "Solid Pattern",
+ "Solid Pattern Tri",
+ "Spots",
+ "Spots Fade",
+ "Glitter",
+ "Candle",
+ "Fireworks Starburst",
+ "Fireworks 1D",
+ "Bouncing Balls",
+ "Sinelon",
+ "Sinelon Dual",
+ "Sinelon Rainbow",
+ "Popcorn",
+ "Drip",
+ "Plasma",
+ "Percent",
+ "Ripple Rainbow",
+ "Heartbeat",
+ "Pacifica",
+ "Candle Multi",
+ "Solid Glitter",
+ "Sunrise",
+ "Phased",
+ "Twinkleup",
+ "Noise Pal",
+ "Sine",
+ "Phased Noise",
+ "Flow",
+ "Chunchun",
+ "Dancing Shadows",
+ "Washing Machine",
+ "Candy Cane",
+ "Blends",
+ "TV Simulator",
+ "Dynamic Smooth"
+ ],
+ "palettes": [
+ "Default",
+ "* Random Cycle",
+ "* Color 1",
+ "* Colors 1&2",
+ "* Color Gradient",
+ "* Colors Only",
+ "Party",
+ "Cloud",
+ "Lava",
+ "Ocean",
+ "Forest",
+ "Rainbow",
+ "Rainbow Bands",
+ "Sunset",
+ "Rivendell",
+ "Breeze",
+ "Red & Blue",
+ "Yellowout",
+ "Analogous",
+ "Splash",
+ "Pastel",
+ "Sunset 2",
+ "Beech",
+ "Vintage",
+ "Departure",
+ "Landscape",
+ "Beach",
+ "Sherbet",
+ "Hult",
+ "Hult 64",
+ "Drywet",
+ "Jul",
+ "Grintage",
+ "Rewhi",
+ "Tertiary",
+ "Fire",
+ "Icefire",
+ "Cyane",
+ "Light Pink",
+ "Autumn",
+ "Magenta",
+ "Magred",
+ "Yelmag",
+ "Yelblu",
+ "Orange & Teal",
+ "Tiamat",
+ "April Night",
+ "Orangery",
+ "C9",
+ "Sakura",
+ "Aurora",
+ "Atlantica",
+ "C9 2",
+ "C9 New",
+ "Temperature",
+ "Aurora 2"
+ ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/wled/rgbw.json b/tests/fixtures/wled/rgbw.json
index ce7033c5888..d5ba9e8d00c 100644
--- a/tests/fixtures/wled/rgbw.json
+++ b/tests/fixtures/wled/rgbw.json
@@ -3,7 +3,7 @@
"on": true,
"bri": 140,
"transition": 7,
- "ps": -1,
+ "ps": 1,
"pl": -1,
"nl": {
"on": false,
@@ -200,5 +200,158 @@
"Orangery",
"C9",
"Sakura"
- ]
+ ],
+ "presets": {
+ "0": {},
+ "1": {
+ "on": false,
+ "bri": 255,
+ "transition": 7,
+ "mainseg": 0,
+ "seg": [
+ {
+ "id": 0,
+ "start": 0,
+ "stop": 13,
+ "grp": 1,
+ "spc": 0,
+ "on": true,
+ "bri": 255,
+ "col": [
+ [
+ 97,
+ 144,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0
+ ]
+ ],
+ "fx": 9,
+ "sx": 183,
+ "ix": 255,
+ "pal": 1,
+ "sel": true,
+ "rev": false,
+ "mi": false
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ }
+ ],
+ "n": "Preset 1"
+ },
+ "2": {
+ "on": false,
+ "bri": 255,
+ "transition": 7,
+ "mainseg": 0,
+ "seg": [
+ {
+ "id": 0,
+ "start": 0,
+ "stop": 13,
+ "grp": 1,
+ "spc": 0,
+ "on": true,
+ "bri": 255,
+ "col": [
+ [
+ 97,
+ 144,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0
+ ]
+ ],
+ "fx": 9,
+ "sx": 183,
+ "ix": 255,
+ "pal": 1,
+ "sel": true,
+ "rev": false,
+ "mi": false
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ },
+ {
+ "stop": 0
+ }
+ ],
+ "n": "Preset 2"
+ }
+ }
}
diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py
index 2290ce9f679..b1cbff83e33 100644
--- a/tests/helpers/test_condition.py
+++ b/tests/helpers/test_condition.py
@@ -1,12 +1,20 @@
"""Test the condition helper."""
from datetime import datetime
-from unittest.mock import patch
+from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components import sun
import homeassistant.components.automation as automation
-from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
+from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ CONF_CONDITION,
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ SUN_EVENT_SUNRISE,
+ SUN_EVENT_SUNSET,
+)
from homeassistant.exceptions import ConditionError, HomeAssistantError
from homeassistant.helpers import condition, trace
from homeassistant.helpers.template import Template
@@ -826,6 +834,90 @@ async def test_time_using_input_datetime(hass):
condition.time(hass, before="input_datetime.not_existing")
+async def test_time_using_sensor(hass):
+ """Test time conditions using sensor entities."""
+ hass.states.async_set(
+ "sensor.am",
+ "2021-06-03 13:00:00.000000+00:00", # 6 am local time
+ {ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP},
+ )
+ hass.states.async_set(
+ "sensor.pm",
+ "2020-06-01 01:00:00.000000+00:00", # 6 pm local time
+ {ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP},
+ )
+ hass.states.async_set(
+ "sensor.no_device_class",
+ "2020-06-01 01:00:00.000000+00:00",
+ )
+ hass.states.async_set(
+ "sensor.invalid_timestamp",
+ "This is not a timestamp",
+ {ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP},
+ )
+
+ with patch(
+ "homeassistant.helpers.condition.dt_util.now",
+ return_value=dt_util.now().replace(hour=3),
+ ):
+ assert not condition.time(hass, after="sensor.am", before="sensor.pm")
+ assert condition.time(hass, after="sensor.pm", before="sensor.am")
+
+ with patch(
+ "homeassistant.helpers.condition.dt_util.now",
+ return_value=dt_util.now().replace(hour=9),
+ ):
+ assert condition.time(hass, after="sensor.am", before="sensor.pm")
+ assert not condition.time(hass, after="sensor.pm", before="sensor.am")
+
+ with patch(
+ "homeassistant.helpers.condition.dt_util.now",
+ return_value=dt_util.now().replace(hour=15),
+ ):
+ assert condition.time(hass, after="sensor.am", before="sensor.pm")
+ assert not condition.time(hass, after="sensor.pm", before="sensor.am")
+
+ with patch(
+ "homeassistant.helpers.condition.dt_util.now",
+ return_value=dt_util.now().replace(hour=21),
+ ):
+ assert not condition.time(hass, after="sensor.am", before="sensor.pm")
+ assert condition.time(hass, after="sensor.pm", before="sensor.am")
+
+ # Trigger on PM time
+ with patch(
+ "homeassistant.helpers.condition.dt_util.now",
+ return_value=dt_util.now().replace(hour=18, minute=0, second=0),
+ ):
+ assert condition.time(hass, after="sensor.pm", before="sensor.am")
+ assert not condition.time(hass, after="sensor.am", before="sensor.pm")
+ assert condition.time(hass, after="sensor.pm")
+ assert not condition.time(hass, before="sensor.pm")
+
+ # Even though valid, the device class is missing
+ assert not condition.time(hass, after="sensor.no_device_class")
+ assert not condition.time(hass, before="sensor.no_device_class")
+
+ # Trigger on AM time
+ with patch(
+ "homeassistant.helpers.condition.dt_util.now",
+ return_value=dt_util.now().replace(hour=6, minute=0, second=0),
+ ):
+ assert not condition.time(hass, after="sensor.pm", before="sensor.am")
+ assert condition.time(hass, after="sensor.am", before="sensor.pm")
+ assert condition.time(hass, after="sensor.am")
+ assert not condition.time(hass, before="sensor.am")
+
+ assert not condition.time(hass, after="sensor.invalid_timestamp")
+ assert not condition.time(hass, before="sensor.invalid_timestamp")
+
+ with pytest.raises(ConditionError):
+ condition.time(hass, after="sensor.not_existing")
+
+ with pytest.raises(ConditionError):
+ condition.time(hass, before="sensor.not_existing")
+
+
async def test_state_raises(hass):
"""Test that state raises ConditionError on errors."""
# No entity
@@ -949,7 +1041,7 @@ async def test_state_attribute(hass):
},
)
- hass.states.async_set("sensor.temperature", 100, {"unkown_attr": 200})
+ hass.states.async_set("sensor.temperature", 100, {"unknown_attr": 200})
with pytest.raises(ConditionError):
test(hass)
@@ -1245,7 +1337,7 @@ async def test_numeric_state_attribute(hass):
},
)
- hass.states.async_set("sensor.temperature", 100, {"unkown_attr": 10})
+ hass.states.async_set("sensor.temperature", 100, {"unknown_attr": 10})
with pytest.raises(ConditionError):
assert test(hass)
@@ -1265,12 +1357,12 @@ async def test_numeric_state_attribute(hass):
async def test_numeric_state_using_input_number(hass):
"""Test numeric_state conditions using input_number entities."""
+ hass.states.async_set("number.low", 10)
await async_setup_component(
hass,
"input_number",
{
"input_number": {
- "low": {"min": 0, "max": 255, "initial": 10},
"high": {"min": 0, "max": 255, "initial": 100},
}
},
@@ -1285,7 +1377,7 @@ async def test_numeric_state_using_input_number(hass):
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": "input_number.high",
- "above": "input_number.low",
+ "above": "number.low",
},
],
},
@@ -1317,10 +1409,10 @@ async def test_numeric_state_using_input_number(hass):
)
assert test(hass)
- hass.states.async_set("input_number.low", "unknown")
+ hass.states.async_set("number.low", "unknown")
assert not test(hass)
- hass.states.async_set("input_number.low", "unavailable")
+ hass.states.async_set("number.low", "unavailable")
assert not test(hass)
with pytest.raises(ConditionError):
@@ -2744,3 +2836,30 @@ async def test_if_action_after_sunset_no_offset_kotzebue(hass, hass_ws_client, c
"sun",
{"result": True, "wanted_time_after": "2015-07-23T11:22:18.467277+00:00"},
)
+
+
+async def test_trigger(hass):
+ """Test trigger condition."""
+ test = await condition.async_from_config(
+ hass,
+ {"alias": "Trigger Cond", "condition": "trigger", "id": "123456"},
+ )
+
+ assert not test(hass)
+ assert not test(hass, {})
+ assert not test(hass, {"other_var": "123456"})
+ assert not test(hass, {"trigger": {"trigger_id": "123456"}})
+ assert test(hass, {"trigger": {"id": "123456"}})
+
+
+async def test_platform_async_validate_condition_config(hass):
+ """Test platform.async_validate_condition_config will be called if it exists."""
+ config = {CONF_DEVICE_ID: "test", CONF_DOMAIN: "test", CONF_CONDITION: "device"}
+ platform = AsyncMock()
+ with patch(
+ "homeassistant.helpers.condition.async_get_device_automation_platform",
+ return_value=platform,
+ ):
+ platform.async_validate_condition_config.return_value = config
+ await condition.async_validate_condition_config(hass, config)
+ platform.async_validate_condition_config.assert_awaited()
diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py
index 232d7bbb8b6..02303825bbd 100644
--- a/tests/helpers/test_config_validation.py
+++ b/tests/helpers/test_config_validation.py
@@ -1024,7 +1024,7 @@ def test_key_value_schemas():
schema(True)
assert str(excinfo.value) == "Expected a dictionary"
- for mode in None, "invalid":
+ for mode in None, {"a": "dict"}, "invalid":
with pytest.raises(vol.Invalid) as excinfo:
schema({"mode": mode})
assert (
diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py
index e134c5e327d..fb7464d405f 100644
--- a/tests/helpers/test_event.py
+++ b/tests/helpers/test_event.py
@@ -3049,6 +3049,27 @@ async def test_async_call_later(hass):
assert remove is mock()
+async def test_async_call_later_timedelta(hass):
+ """Test calling an action later with a timedelta."""
+
+ def action():
+ pass
+
+ now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC)
+
+ with patch(
+ "homeassistant.helpers.event.async_track_point_in_utc_time"
+ ) as mock, patch("homeassistant.util.dt.utcnow", return_value=now):
+ remove = async_call_later(hass, timedelta(seconds=3), action)
+
+ assert len(mock.mock_calls) == 1
+ p_hass, p_action, p_point = mock.mock_calls[0][1]
+ assert p_hass is hass
+ assert p_action is action
+ assert p_point == now + timedelta(seconds=3)
+ assert remove is mock()
+
+
async def test_track_state_change_event_chain_multple_entity(hass):
"""Test that adding a new state tracker inside a tracker does not fire right away."""
tracker_called = []
diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py
index b198a16adb1..5e48b2aec5f 100644
--- a/tests/helpers/test_frame.py
+++ b/tests/helpers/test_frame.py
@@ -15,7 +15,7 @@ async def test_extract_frame_integration(caplog, mock_integration_frame):
assert found_frame == mock_integration_frame
-async def test_extract_frame_integration_with_excluded_intergration(caplog):
+async def test_extract_frame_integration_with_excluded_integration(caplog):
"""Test extracting the current frame from integration context."""
correct_frame = Mock(
filename="/home/dev/homeassistant/components/mdns/light.py",
diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py
index 546f494735e..dfa5ce34ce7 100644
--- a/tests/helpers/test_script.py
+++ b/tests/helpers/test_script.py
@@ -6,7 +6,7 @@ from datetime import timedelta
import logging
from types import MappingProxyType
from unittest import mock
-from unittest.mock import patch
+from unittest.mock import AsyncMock, patch
from async_timeout import timeout
import pytest
@@ -15,7 +15,12 @@ import voluptuous as vol
# Otherwise can't test just this file (import order issue)
from homeassistant import exceptions
import homeassistant.components.scene as scene
-from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ SERVICE_TURN_ON,
+)
from homeassistant.core import SERVICE_CALL_LIMIT, Context, CoreState, callback
from homeassistant.exceptions import ConditionError, ServiceNotFound
from homeassistant.helpers import config_validation as cv, script, trace
@@ -477,7 +482,7 @@ async def test_stop_no_wait(hass, count):
# Can't assert just yet because we haven't verified stopping works yet.
# If assert fails we can hang test if async_stop doesn't work.
- script_was_runing = script_obj.is_running
+ script_was_running = script_obj.is_running
were_no_events = len(events) == 0
# Begin the process of stopping the script (which should stop all runs), and then
@@ -487,7 +492,7 @@ async def test_stop_no_wait(hass, count):
await hass.async_block_till_done()
- assert script_was_runing
+ assert script_was_running
assert were_no_events
assert not script_obj.is_running
assert len(events) == 0
@@ -1189,8 +1194,8 @@ async def test_wait_template_with_utcnow(hass):
start_time = dt_util.utcnow().replace(minute=1) + timedelta(hours=48)
try:
- non_maching_time = start_time.replace(hour=3)
- with patch("homeassistant.util.dt.utcnow", return_value=non_maching_time):
+ non_matching_time = start_time.replace(hour=3)
+ with patch("homeassistant.util.dt.utcnow", return_value=non_matching_time):
hass.async_create_task(script_obj.async_run(context=Context()))
await asyncio.wait_for(wait_started_flag.wait(), 1)
assert script_obj.is_running
@@ -1221,17 +1226,17 @@ async def test_wait_template_with_utcnow_no_match(hass):
timed_out = False
try:
- non_maching_time = start_time.replace(hour=3)
- with patch("homeassistant.util.dt.utcnow", return_value=non_maching_time):
+ non_matching_time = start_time.replace(hour=3)
+ with patch("homeassistant.util.dt.utcnow", return_value=non_matching_time):
hass.async_create_task(script_obj.async_run(context=Context()))
await asyncio.wait_for(wait_started_flag.wait(), 1)
assert script_obj.is_running
- second_non_maching_time = start_time.replace(hour=4)
+ second_non_matching_time = start_time.replace(hour=4)
with patch(
- "homeassistant.util.dt.utcnow", return_value=second_non_maching_time
+ "homeassistant.util.dt.utcnow", return_value=second_non_matching_time
):
- async_fire_time_changed(hass, second_non_maching_time)
+ async_fire_time_changed(hass, second_non_matching_time)
with timeout(0.1):
await hass.async_block_till_done()
@@ -3130,3 +3135,16 @@ async def test_breakpoints_2(hass):
assert not script_obj.is_running
assert script_obj.runs == 0
assert len(events) == 1
+
+
+async def test_platform_async_validate_action_config(hass):
+ """Test platform.async_validate_action_config will be called if it exists."""
+ config = {CONF_DEVICE_ID: "test", CONF_DOMAIN: "test"}
+ platform = AsyncMock()
+ with patch(
+ "homeassistant.helpers.script.device_automation.async_get_device_automation_platform",
+ return_value=platform,
+ ):
+ platform.async_validate_action_config.return_value = config
+ await script.async_validate_action_config(hass, config)
+ platform.async_validate_action_config.assert_awaited()
diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py
index 48ef6f25b67..2547537bff9 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -526,6 +526,33 @@ def test_timestamp_local(hass):
)
+@pytest.mark.parametrize(
+ "input",
+ (
+ "2021-06-03 13:00:00.000000+00:00",
+ "1986-07-09T12:00:00Z",
+ "2016-10-19 15:22:05.588122+0100",
+ "2016-10-19",
+ "2021-01-01 00:00:01",
+ "invalid",
+ ),
+)
+def test_as_datetime(hass, input):
+ """Test converting a timestamp string to a date object."""
+ expected = dt_util.parse_datetime(input)
+ if expected is not None:
+ expected = str(expected)
+
+ assert (
+ template.Template(f"{{{{ as_datetime('{input}') }}}}", hass).async_render()
+ == expected
+ )
+ assert (
+ template.Template(f"{{{{ '{input}' | as_datetime }}}}", hass).async_render()
+ == expected
+ )
+
+
def test_as_local(hass):
"""Test converting time to local."""
@@ -2472,7 +2499,7 @@ async def test_no_result_parsing(hass):
async def test_is_static_still_ast_evals(hass):
- """Test is_static still convers to native type."""
+ """Test is_static still converts to native type."""
tpl = template.Template("[1, 2]", hass)
assert tpl.is_static
assert tpl.async_render() == [1, 2]
diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py
index 556f06fce54..7bcc83048a4 100644
--- a/tests/test_config_entries.py
+++ b/tests/test_config_entries.py
@@ -9,6 +9,7 @@ import pytest
from homeassistant import config_entries, data_entry_flow, loader
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CoreState, callback
+from homeassistant.data_entry_flow import RESULT_TYPE_ABORT
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
@@ -336,6 +337,30 @@ async def test_remove_entry(hass, manager):
assert not entity_entry_list
+async def test_remove_entry_cancels_reauth(hass, manager):
+ """Tests that removing a config entry, also aborts existing reauth flows."""
+ entry = MockConfigEntry(title="test_title", domain="test")
+
+ mock_setup_entry = AsyncMock(side_effect=ConfigEntryAuthFailed())
+ mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry))
+ mock_entity_platform(hass, "config_flow.test", None)
+
+ entry.add_to_hass(hass)
+ await entry.async_setup(hass)
+ await hass.async_block_till_done()
+
+ flows = hass.config_entries.flow.async_progress()
+ assert len(flows) == 1
+ assert flows[0]["context"]["entry_id"] == entry.entry_id
+ assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH
+ assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR
+
+ await manager.async_remove(entry.entry_id)
+
+ flows = hass.config_entries.flow.async_progress()
+ assert len(flows) == 0
+
+
async def test_remove_entry_handles_callback_error(hass, manager):
"""Test that exceptions in the remove callback are handled."""
mock_setup_entry = AsyncMock(return_value=True)
@@ -1626,7 +1651,7 @@ async def test_unique_id_update_existing_entry_without_reload(hass, manager):
)
await hass.async_block_till_done()
- assert result["type"] == "abort"
+ assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
assert entry.data["host"] == "1.1.1.1"
assert entry.data["additional"] == "data"
@@ -1671,7 +1696,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager):
)
await hass.async_block_till_done()
- assert result["type"] == "abort"
+ assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
assert entry.data["host"] == "1.1.1.1"
assert entry.data["additional"] == "data"
@@ -1688,7 +1713,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager):
)
await hass.async_block_till_done()
- assert result["type"] == "abort"
+ assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
assert entry.data["host"] == "2.2.2.2"
assert entry.data["additional"] == "data"
@@ -1731,7 +1756,7 @@ async def test_unique_id_not_update_existing_entry(hass, manager):
)
await hass.async_block_till_done()
- assert result["type"] == "abort"
+ assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
assert entry.data["host"] == "0.0.0.0"
assert entry.data["additional"] == "data"
@@ -1970,7 +1995,7 @@ async def test__async_current_entries_does_not_skip_ignore_non_user(hass, manage
assert len(mock_setup_entry.mock_calls) == 0
-async def test__async_current_entries_explict_skip_ignore(hass, manager):
+async def test__async_current_entries_explicit_skip_ignore(hass, manager):
"""Test that _async_current_entries can explicitly include ignore."""
hass.config.components.add("comp")
entry = MockConfigEntry(
@@ -2009,7 +2034,7 @@ async def test__async_current_entries_explict_skip_ignore(hass, manager):
assert p_entry.data == {"token": "supersecret"}
-async def test__async_current_entries_explict_include_ignore(hass, manager):
+async def test__async_current_entries_explicit_include_ignore(hass, manager):
"""Test that _async_current_entries can explicitly include ignore."""
hass.config.components.add("comp")
entry = MockConfigEntry(
@@ -2845,7 +2870,7 @@ async def test__async_abort_entries_match(hass, manager, matchers, reason):
)
await hass.async_block_till_done()
- assert result["type"] == "abort"
+ assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == reason
diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py
index 34b07a2a871..4b5777d86f8 100644
--- a/tests/test_data_entry_flow.py
+++ b/tests/test_data_entry_flow.py
@@ -121,7 +121,7 @@ async def test_show_form(manager):
)
form = await manager.async_init("test")
- assert form["type"] == "form"
+ assert form["type"] == data_entry_flow.RESULT_TYPE_FORM
assert form["data_schema"] is schema
assert form["errors"] == {"username": "Should be unique."}
@@ -369,7 +369,7 @@ async def test_abort_flow_exception(manager):
raise data_entry_flow.AbortFlow("mock-reason", {"placeholder": "yo"})
form = await manager.async_init("test")
- assert form["type"] == "abort"
+ assert form["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert form["reason"] == "mock-reason"
assert form["description_placeholders"] == {"placeholder": "yo"}
diff --git a/tests/test_requirements.py b/tests/test_requirements.py
index f68601e889e..26f3603910d 100644
--- a/tests/test_requirements.py
+++ b/tests/test_requirements.py
@@ -364,7 +364,11 @@ async def test_discovery_requirements_ssdp(hass):
assert len(mock_process.mock_calls) == 4
assert mock_process.mock_calls[0][1][2] == ssdp.requirements
# Ensure zeroconf is a dep for ssdp
- assert mock_process.mock_calls[1][1][1] == "zeroconf"
+ assert {
+ mock_process.mock_calls[1][1][1],
+ mock_process.mock_calls[2][1][1],
+ mock_process.mock_calls[3][1][1],
+ } == {"network", "zeroconf", "http"}
@pytest.mark.parametrize(
diff --git a/tests/util/test_executor.py b/tests/util/test_executor.py
index 911145ecc4e..eaa48c75d1a 100644
--- a/tests/util/test_executor.py
+++ b/tests/util/test_executor.py
@@ -24,7 +24,7 @@ async def test_executor_shutdown_can_interrupt_threads(caplog):
for _ in range(100):
sleep_futures.append(iexecutor.submit(_loop_sleep_in_executor))
- iexecutor.logged_shutdown()
+ iexecutor.shutdown()
for future in sleep_futures:
with pytest.raises((concurrent.futures.CancelledError, SystemExit)):
@@ -45,13 +45,13 @@ async def test_executor_shutdown_only_logs_max_attempts(caplog):
iexecutor.submit(_loop_sleep_in_executor)
with patch.object(executor, "EXECUTOR_SHUTDOWN_TIMEOUT", 0.3):
- iexecutor.logged_shutdown()
+ iexecutor.shutdown()
assert "time.sleep(0.2)" in caplog.text
assert (
caplog.text.count("is still running at shutdown") == executor.MAX_LOG_ATTEMPTS
)
- iexecutor.logged_shutdown()
+ iexecutor.shutdown()
async def test_executor_shutdown_does_not_log_shutdown_on_first_attempt(caplog):
@@ -65,7 +65,7 @@ async def test_executor_shutdown_does_not_log_shutdown_on_first_attempt(caplog):
for _ in range(5):
iexecutor.submit(_do_nothing)
- iexecutor.logged_shutdown()
+ iexecutor.shutdown()
assert "is still running at shutdown" not in caplog.text
@@ -83,9 +83,9 @@ async def test_overall_timeout_reached(caplog):
start = time.monotonic()
with patch.object(executor, "EXECUTOR_SHUTDOWN_TIMEOUT", 0.5):
- iexecutor.logged_shutdown()
+ iexecutor.shutdown()
finish = time.monotonic()
assert finish - start < 1
- iexecutor.logged_shutdown()
+ iexecutor.shutdown()
diff --git a/tests/util/test_temperature.py b/tests/util/test_temperature.py
new file mode 100644
index 00000000000..7730a89cbb8
--- /dev/null
+++ b/tests/util/test_temperature.py
@@ -0,0 +1,84 @@
+"""Test Home Assistant temperature utility functions."""
+import pytest
+
+from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN
+import homeassistant.util.temperature as temperature_util
+
+INVALID_SYMBOL = "bob"
+VALID_SYMBOL = TEMP_CELSIUS
+
+
+def test_convert_same_unit():
+ """Test conversion from any unit to same unit."""
+ assert temperature_util.convert(2, TEMP_CELSIUS, TEMP_CELSIUS) == 2
+ assert temperature_util.convert(3, TEMP_FAHRENHEIT, TEMP_FAHRENHEIT) == 3
+ assert temperature_util.convert(4, TEMP_KELVIN, TEMP_KELVIN) == 4
+
+
+def test_convert_invalid_unit():
+ """Test exception is thrown for invalid units."""
+ with pytest.raises(ValueError):
+ temperature_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL)
+
+ with pytest.raises(ValueError):
+ temperature_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL)
+
+
+def test_convert_nonnumeric_value():
+ """Test exception is thrown for nonnumeric type."""
+ with pytest.raises(TypeError):
+ temperature_util.convert("a", TEMP_CELSIUS, TEMP_FAHRENHEIT)
+
+
+def test_convert_from_celsius():
+ """Test conversion from C to other units."""
+ celsius = 100
+ assert temperature_util.convert(
+ celsius, TEMP_CELSIUS, TEMP_FAHRENHEIT
+ ) == pytest.approx(212.0)
+ assert temperature_util.convert(
+ celsius, TEMP_CELSIUS, TEMP_KELVIN
+ ) == pytest.approx(373.15)
+ # Interval
+ assert temperature_util.convert(
+ celsius, TEMP_CELSIUS, TEMP_FAHRENHEIT, True
+ ) == pytest.approx(180.0)
+ assert temperature_util.convert(
+ celsius, TEMP_CELSIUS, TEMP_KELVIN, True
+ ) == pytest.approx(100)
+
+
+def test_convert_from_fahrenheit():
+ """Test conversion from F to other units."""
+ fahrenheit = 100
+ assert temperature_util.convert(
+ fahrenheit, TEMP_FAHRENHEIT, TEMP_CELSIUS
+ ) == pytest.approx(37.77777777777778)
+ assert temperature_util.convert(
+ fahrenheit, TEMP_FAHRENHEIT, TEMP_KELVIN
+ ) == pytest.approx(310.92777777777775)
+ # Interval
+ assert temperature_util.convert(
+ fahrenheit, TEMP_FAHRENHEIT, TEMP_CELSIUS, True
+ ) == pytest.approx(55.55555555555556)
+ assert temperature_util.convert(
+ fahrenheit, TEMP_FAHRENHEIT, TEMP_KELVIN, True
+ ) == pytest.approx(55.55555555555556)
+
+
+def test_convert_from_kelvin():
+ """Test conversion from K to other units."""
+ kelvin = 100
+ assert temperature_util.convert(kelvin, TEMP_KELVIN, TEMP_CELSIUS) == pytest.approx(
+ -173.15
+ )
+ assert temperature_util.convert(
+ kelvin, TEMP_KELVIN, TEMP_FAHRENHEIT
+ ) == pytest.approx(-279.66999999999996)
+ # Interval
+ assert temperature_util.convert(
+ kelvin, TEMP_KELVIN, TEMP_FAHRENHEIT, True
+ ) == pytest.approx(180.0)
+ assert temperature_util.convert(
+ kelvin, TEMP_KELVIN, TEMP_KELVIN, True
+ ) == pytest.approx(100)
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index 74abfef452f..f32e731f9b3 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -106,7 +106,7 @@ def test_temperature_same_unit():
def test_temperature_unknown_unit():
"""Test no conversion happens if unknown unit."""
with pytest.raises(ValueError):
- METRIC_SYSTEM.temperature(5, "K")
+ METRIC_SYSTEM.temperature(5, "abc")
def test_temperature_to_metric():