diff --git a/.coveragerc b/.coveragerc index a5d7eec9115..bd19ab31b52 100644 --- a/.coveragerc +++ b/.coveragerc @@ -29,10 +29,8 @@ omit = homeassistant/components/ads/* homeassistant/components/aemet/weather_update_coordinator.py homeassistant/components/aftership/* - homeassistant/components/agent_dvr/__init__.py homeassistant/components/agent_dvr/alarm_control_panel.py homeassistant/components/agent_dvr/camera.py - homeassistant/components/agent_dvr/const.py homeassistant/components/agent_dvr/helpers.py homeassistant/components/airnow/__init__.py homeassistant/components/airnow/sensor.py @@ -85,7 +83,6 @@ omit = homeassistant/components/aurora/binary_sensor.py homeassistant/components/aurora/const.py homeassistant/components/aurora/sensor.py - homeassistant/components/aurora_abb_powerone/sensor.py homeassistant/components/avea/light.py homeassistant/components/avion/light.py homeassistant/components/azure_devops/__init__.py @@ -134,6 +131,7 @@ omit = homeassistant/components/braviatv/remote.py homeassistant/components/broadlink/__init__.py homeassistant/components/broadlink/const.py + homeassistant/components/broadlink/light.py homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/switch.py homeassistant/components/broadlink/updater.py @@ -269,7 +267,10 @@ omit = homeassistant/components/enphase_envoy/__init__.py homeassistant/components/enphase_envoy/sensor.py homeassistant/components/entur_public_transport/* - homeassistant/components/environment_canada/* + homeassistant/components/environment_canada/__init__.py + homeassistant/components/environment_canada/camera.py + homeassistant/components/environment_canada/sensor.py + homeassistant/components/environment_canada/weather.py homeassistant/components/envirophat/sensor.py homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py @@ -290,7 +291,6 @@ omit = homeassistant/components/esphome/select.py homeassistant/components/esphome/sensor.py homeassistant/components/esphome/switch.py - homeassistant/components/essent/sensor.py homeassistant/components/etherscan/sensor.py homeassistant/components/eufy/* homeassistant/components/everlights/light.py @@ -299,6 +299,7 @@ omit = homeassistant/components/ezviz/camera.py homeassistant/components/ezviz/coordinator.py homeassistant/components/ezviz/const.py + homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/binary_sensor.py homeassistant/components/ezviz/sensor.py homeassistant/components/ezviz/switch.py @@ -331,6 +332,7 @@ omit = homeassistant/components/fjaraskupan/const.py homeassistant/components/fjaraskupan/fan.py homeassistant/components/fjaraskupan/light.py + homeassistant/components/fjaraskupan/number.py homeassistant/components/fjaraskupan/sensor.py homeassistant/components/fleetgo/device_tracker.py homeassistant/components/flexit/climate.py @@ -343,7 +345,6 @@ omit = homeassistant/components/flume/sensor.py homeassistant/components/flunearyou/__init__.py homeassistant/components/flunearyou/sensor.py - homeassistant/components/flux_led/light.py homeassistant/components/folder/sensor.py homeassistant/components/folder_watcher/* homeassistant/components/foobot/sensor.py @@ -457,7 +458,6 @@ omit = homeassistant/components/hp_ilo/sensor.py homeassistant/components/htu21d/sensor.py homeassistant/components/huawei_lte/* - homeassistant/components/huawei_router/device_tracker.py homeassistant/components/hue/light.py homeassistant/components/hunterdouglas_powerview/__init__.py homeassistant/components/hunterdouglas_powerview/scene.py @@ -585,6 +585,11 @@ omit = homeassistant/components/logi_circle/const.py homeassistant/components/logi_circle/sensor.py homeassistant/components/london_underground/sensor.py + homeassistant/components/lookin/__init__.py + homeassistant/components/lookin/entity.py + homeassistant/components/lookin/models.py + homeassistant/components/lookin/sensor.py + homeassistant/components/lookin/climate.py homeassistant/components/loopenergy/sensor.py homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/__init__.py @@ -599,7 +604,6 @@ omit = homeassistant/components/lutron_caseta/scene.py homeassistant/components/lutron_caseta/switch.py homeassistant/components/lw12wifi/light.py - homeassistant/components/lyft/sensor.py homeassistant/components/lyric/__init__.py homeassistant/components/lyric/api.py homeassistant/components/lyric/climate.py @@ -649,13 +653,7 @@ omit = homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py homeassistant/components/mochad/* - homeassistant/components/modbus/base_platform.py - homeassistant/components/modbus/binary_sensor.py - homeassistant/components/modbus/cover.py homeassistant/components/modbus/climate.py - homeassistant/components/modbus/modbus.py - homeassistant/components/modbus/sensor.py - homeassistant/components/modbus/validators.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py @@ -703,7 +701,6 @@ omit = homeassistant/components/neato/switch.py homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py - homeassistant/components/nello/lock.py homeassistant/components/nest/legacy/* homeassistant/components/netdata/sensor.py homeassistant/components/netgear/__init__.py @@ -734,11 +731,10 @@ omit = homeassistant/components/nuki/const.py homeassistant/components/nuki/binary_sensor.py homeassistant/components/nuki/lock.py - homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nzbget/coordinator.py homeassistant/components/obihai/* - homeassistant/components/octoprint/* + homeassistant/components/octoprint/__init__.py homeassistant/components/oem/climate.py homeassistant/components/oasa_telematics/sensor.py homeassistant/components/ohmconnect/sensor.py @@ -765,7 +761,10 @@ omit = homeassistant/components/openevse/sensor.py homeassistant/components/openexchangerates/sensor.py homeassistant/components/opengarage/__init__.py + homeassistant/components/opengarage/binary_sensor.py homeassistant/components/opengarage/cover.py + homeassistant/components/opengarage/entity.py + homeassistant/components/opengarage/sensor.py homeassistant/components/openhome/__init__.py homeassistant/components/openhome/media_player.py homeassistant/components/openhome/const.py @@ -781,7 +780,6 @@ omit = homeassistant/components/openweathermap/sensor.py homeassistant/components/openweathermap/weather.py homeassistant/components/openweathermap/weather_update_coordinator.py - homeassistant/components/openweathermap/abstract_owm_sensor.py homeassistant/components/opnsense/* homeassistant/components/opple/light.py homeassistant/components/orangepi_gpio/* @@ -907,6 +905,7 @@ omit = homeassistant/components/screenlogic/__init__.py homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py + homeassistant/components/screenlogic/light.py homeassistant/components/screenlogic/sensor.py homeassistant/components/screenlogic/services.py homeassistant/components/screenlogic/switch.py @@ -1002,7 +1001,8 @@ omit = homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* - homeassistant/components/stookalert/* + homeassistant/components/stookalert/__init__.py + homeassistant/components/stookalert/binary_sensor.py homeassistant/components/stream/* homeassistant/components/streamlabswater/* homeassistant/components/suez_water/* @@ -1106,7 +1106,14 @@ omit = homeassistant/components/tractive/entity.py homeassistant/components/tractive/sensor.py homeassistant/components/tractive/switch.py - homeassistant/components/tradfri/* + homeassistant/components/tradfri/__init__.py + homeassistant/components/tradfri/base_class.py + homeassistant/components/tradfri/config_flow.py + homeassistant/components/tradfri/cover.py + homeassistant/components/tradfri/fan.py + homeassistant/components/tradfri/light.py + homeassistant/components/tradfri/sensor.py + homeassistant/components/tradfri/switch.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/transmission/sensor.py @@ -1116,12 +1123,22 @@ omit = homeassistant/components/travisci/sensor.py homeassistant/components/tuya/__init__.py homeassistant/components/tuya/base.py + homeassistant/components/tuya/binary_sensor.py + homeassistant/components/tuya/camera.py homeassistant/components/tuya/climate.py homeassistant/components/tuya/const.py + homeassistant/components/tuya/cover.py homeassistant/components/tuya/fan.py + homeassistant/components/tuya/humidifier.py homeassistant/components/tuya/light.py + homeassistant/components/tuya/number.py homeassistant/components/tuya/scene.py + homeassistant/components/tuya/select.py + homeassistant/components/tuya/sensor.py + homeassistant/components/tuya/siren.py homeassistant/components/tuya/switch.py + homeassistant/components/tuya/util.py + homeassistant/components/tuya/vacuum.py homeassistant/components/twentemilieu/const.py homeassistant/components/twentemilieu/sensor.py homeassistant/components/twilio_call/notify.py @@ -1151,6 +1168,7 @@ omit = homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py homeassistant/components/velux/* + homeassistant/components/venstar/__init__.py homeassistant/components/venstar/climate.py homeassistant/components/verisure/__init__.py homeassistant/components/verisure/alarm_control_panel.py @@ -1174,6 +1192,7 @@ omit = homeassistant/components/vilfo/const.py homeassistant/components/vivotek/camera.py homeassistant/components/vlc/media_player.py + homeassistant/components/vlc_telnet/__init__.py homeassistant/components/vlc_telnet/media_player.py homeassistant/components/volkszaehler/sensor.py homeassistant/components/volumio/__init__.py @@ -1193,7 +1212,6 @@ omit = homeassistant/components/webostv/* homeassistant/components/whois/sensor.py homeassistant/components/wiffi/* - homeassistant/components/wink/* homeassistant/components/wirelesstag/* homeassistant/components/wolflink/__init__.py homeassistant/components/wolflink/sensor.py @@ -1236,7 +1254,6 @@ omit = homeassistant/components/xiaomi_miio/select.py homeassistant/components/xiaomi_miio/sensor.py homeassistant/components/xiaomi_miio/switch.py - homeassistant/components/xiaomi_miio/vacuum.py homeassistant/components/xiaomi_tv/media_player.py homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 116afec36ee..ac4c8453327 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -15,7 +15,7 @@ body: attributes: label: The problem description: >- - Describe the issue you are experiencing here to communicate to the + Describe the issue you are experiencing here, to communicate to the maintainers. Tell us what you were trying to do and what happened. Provide a clear and concise description of what the problem is. @@ -28,10 +28,12 @@ body: validations: required: true attributes: - label: What is version of Home Assistant Core has the issue? + label: What version of Home Assistant Core has the issue? placeholder: core- description: > - Can be found in the Configuration panel -> Info. + Can be found in: [Configuration panel -> Info](https://my.home-assistant.io/redirect/info/). + + [![Open your Home Assistant instance and show your Home Assistant version information.](https://my.home-assistant.io/badges/info.svg)](https://my.home-assistant.io/redirect/info/) - type: input attributes: label: What was the last working version of Home Assistant Core? @@ -44,7 +46,9 @@ body: attributes: label: What type of installation are you running? description: > - If you don't know, you can find it in: Configuration panel -> Info. + Can be found in: [Configuration panel -> Info](https://my.home-assistant.io/redirect/info/). + + [![Open your Home Assistant instance and show your Home Assistant version information.](https://my.home-assistant.io/badges/info.svg)](https://my.home-assistant.io/redirect/info/) options: - Home Assistant OS - Home Assistant Container @@ -55,15 +59,15 @@ body: attributes: label: Integration causing the issue description: > - The name of the integration, for example, Automation or Philips Hue. + The name of the integration. For example: Automation, Philips Hue - type: input id: integration_link attributes: label: Link to integration documentation on our website placeholder: "https://www.home-assistant.io/integrations/..." description: | - Providing a link [to the documentation][docs] help us categorizing the - issue, while providing a useful reference at the same time. + Providing a link [to the documentation][docs] helps us categorize the + issue, while also providing a useful reference for others. [docs]: https://www.home-assistant.io/integrations @@ -75,8 +79,8 @@ body: attributes: label: Example YAML snippet description: | - If this issue has an example piece of YAML that can help reproducing this problem, please provide. - This can be an piece of YAML from, e.g., an automation, script, scene or configuration. + If applicable, please provide an example piece of YAML that can help reproduce this problem. + This can be from an automation, script, scene or configuration. render: yaml - type: textarea attributes: @@ -88,5 +92,3 @@ body: label: Additional information description: > If you have any additional information for us, use the field below. - Please note, you can attach screenshots or screen recordings here, by - dragging and dropping files in the field below. diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index d8558c6fdff..708e360d59b 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -23,7 +23,7 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 with: fetch-depth: 0 @@ -67,7 +67,7 @@ jobs: if: needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 @@ -97,7 +97,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' @@ -170,7 +170,7 @@ jobs: - tinker steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Login to DockerHub uses: docker/login-action@v1.10.0 @@ -201,7 +201,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -233,7 +233,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Login to DockerHub uses: docker/login-action@v1.10.0 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a04e815af7f..aa5a6ddd3ca 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,7 +26,7 @@ jobs: pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v2.2.2 @@ -84,7 +84,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -124,7 +124,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -164,7 +164,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -207,7 +207,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Register hadolint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/hadolint.json" @@ -226,7 +226,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -269,7 +269,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -312,7 +312,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -352,7 +352,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -395,7 +395,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -436,7 +436,7 @@ jobs: # needs: prepare-base # steps: # - name: Check out code from GitHub - # uses: actions/checkout@v2.3.4 + # uses: actions/checkout@v2.3.5 # - name: Run ShellCheck # uses: ludeeus/action-shellcheck@0.3.0 @@ -446,7 +446,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -493,7 +493,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 @@ -517,7 +517,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -551,7 +551,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Generate partial Python venv restore key id: generate-python-key run: >- @@ -595,7 +595,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 @@ -626,7 +626,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 @@ -660,7 +660,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 @@ -682,11 +682,11 @@ jobs: # Ideally this should be part of our dependencies # However this plugin is fairly new and doesn't run correctly # on a non-GitHub environment. - pip install pytest-github-actions-annotate-failures + pip install pytest-github-actions-annotate-failures==0.1.3 - name: Run pytest run: | . venv/bin/activate - pytest \ + python3 -X dev -bb -m pytest \ -qq \ --timeout=9 \ --durations=10 \ @@ -718,7 +718,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index d2ccdc6c9ca..6e734528f0e 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 9cced377f8c..ebb4a65b80e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -21,7 +21,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Get information id: info @@ -68,7 +68,7 @@ jobs: - "3.9-alpine3.14" steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Download env_file uses: actions/download-artifact@v2 @@ -108,7 +108,7 @@ jobs: - "3.9-alpine3.14" steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Download env_file uses: actions/download-artifact@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a9424e53b8..ef58733100b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,17 +22,17 @@ repos: - --quiet-level=2 exclude_types: [csv, json] exclude: ^tests/fixtures/ - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: - - pycodestyle==2.7.0 - - pyflakes==2.3.1 + - pycodestyle==2.8.0 + - pyflakes==2.4.0 - flake8-docstrings==1.6.0 - - pydocstyle==6.0.0 - - flake8-comprehensions==3.5.0 - - flake8-noqa==1.1.0 + - pydocstyle==6.1.1 + - flake8-comprehensions==3.7.0 + - flake8-noqa==1.2.0 - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit diff --git a/.strict-typing b/.strict-typing index b5be241ac00..685c87aa094 100644 --- a/.strict-typing +++ b/.strict-typing @@ -20,6 +20,7 @@ homeassistant.components.ampio.* homeassistant.components.automation.* homeassistant.components.binary_sensor.* homeassistant.components.bluetooth_tracker.* +homeassistant.components.bmw_connected_drive.* homeassistant.components.bond.* homeassistant.components.braviatv.* homeassistant.components.brother.* @@ -35,18 +36,21 @@ homeassistant.components.dlna_dmr.* homeassistant.components.dnsip.* homeassistant.components.dsmr.* homeassistant.components.dunehd.* +homeassistant.components.efergy.* homeassistant.components.elgato.* homeassistant.components.esphome.* homeassistant.components.energy.* homeassistant.components.fastdotcom.* homeassistant.components.fitbit.* homeassistant.components.flunearyou.* +homeassistant.components.flux_led.* homeassistant.components.forecast_solar.* homeassistant.components.fritzbox.* homeassistant.components.frontend.* homeassistant.components.fritz.* homeassistant.components.geo_location.* homeassistant.components.gios.* +homeassistant.components.goalzero.* homeassistant.components.group.* homeassistant.components.guardian.* homeassistant.components.history.* @@ -55,23 +59,30 @@ homeassistant.components.http.* homeassistant.components.huawei_lte.* homeassistant.components.hyperion.* homeassistant.components.image_processing.* +homeassistant.components.input_select.* homeassistant.components.integration.* homeassistant.components.iqvia.* +homeassistant.components.jewish_calendar.* homeassistant.components.knx.* homeassistant.components.kraken.* homeassistant.components.lcn.* homeassistant.components.light.* homeassistant.components.local_ip.* homeassistant.components.lock.* +homeassistant.components.lookin.* homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.modbus.* +homeassistant.components.modem_callerid.* +homeassistant.components.media_source.* homeassistant.components.mysensors.* homeassistant.components.nam.* +homeassistant.components.nanoleaf.* homeassistant.components.neato.* homeassistant.components.nest.* homeassistant.components.netatmo.* homeassistant.components.network.* +homeassistant.components.nfandroidtv.* homeassistant.components.no_ip.* homeassistant.components.notify.* homeassistant.components.notion.* @@ -89,6 +100,7 @@ homeassistant.components.recorder.statistics homeassistant.components.remote.* homeassistant.components.renault.* homeassistant.components.rituals_perfume_genie.* +homeassistant.components.rpi_power.* homeassistant.components.samsungtv.* homeassistant.components.scene.* homeassistant.components.select.* @@ -98,6 +110,7 @@ homeassistant.components.simplisafe.* homeassistant.components.slack.* homeassistant.components.sonos.media_player homeassistant.components.ssdp.* +homeassistant.components.stookalert.* homeassistant.components.stream.* homeassistant.components.sun.* homeassistant.components.surepetcare.* @@ -110,6 +123,7 @@ homeassistant.components.tautulli.* homeassistant.components.tcp.* homeassistant.components.tile.* homeassistant.components.tplink.* +homeassistant.components.tractive.* homeassistant.components.tradfri.* homeassistant.components.tts.* homeassistant.components.upcloud.* @@ -118,6 +132,7 @@ homeassistant.components.uptimerobot.* homeassistant.components.vacuum.* homeassistant.components.vallox.* homeassistant.components.water_heater.* +homeassistant.components.watttime.* homeassistant.components.weather.* homeassistant.components.websocket_api.* homeassistant.components.zodiac.* diff --git a/CODEOWNERS b/CODEOWNERS index a43b38e509d..2612cc3fd18 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -75,10 +75,10 @@ homeassistant/components/blink/* @fronzbot homeassistant/components/blueprint/* @home-assistant/core homeassistant/components/bmp280/* @belidzs homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe -homeassistant/components/bond/* @prystupa @joshs85 +homeassistant/components/bond/* @bdraco @prystupa @joshs85 homeassistant/components/bosch_shc/* @tschamm homeassistant/components/braviatv/* @bieniu @Drafteed -homeassistant/components/broadlink/* @danielhiversen @felipediel +homeassistant/components/broadlink/* @danielhiversen @felipediel @L-I-Am homeassistant/components/brother/* @bieniu homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bsblan/* @liudger @@ -114,7 +114,7 @@ homeassistant/components/debugpy/* @frenck homeassistant/components/deconz/* @Kane610 homeassistant/components/delijn/* @bollewolle @Emilv2 homeassistant/components/demo/* @home-assistant/core -homeassistant/components/denonavr/* @scarface-4711 @starkillerOG +homeassistant/components/denonavr/* @ol-iver @starkillerOG homeassistant/components/derivative/* @afaucogney homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/devolo_home_control/* @2Fake @Shutgun @@ -138,7 +138,7 @@ homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/edl21/* @mtdcr homeassistant/components/efergy/* @tkdrob homeassistant/components/egardia/* @jeroenterheerdt -homeassistant/components/eight_sleep/* @mezz64 +homeassistant/components/eight_sleep/* @mezz64 @raman325 homeassistant/components/elgato/* @frenck homeassistant/components/elkm1/* @gwww @bdraco homeassistant/components/elv/* @majuss @@ -151,13 +151,12 @@ homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer homeassistant/components/enphase_envoy/* @gtdiehl homeassistant/components/entur_public_transport/* @hfurubotten -homeassistant/components/environment_canada/* @michaeldavie +homeassistant/components/environment_canada/* @gwww @michaeldavie homeassistant/components/ephember/* @ttroy50 homeassistant/components/epson/* @pszafer homeassistant/components/epsonworkforce/* @ThaStealth homeassistant/components/eq3btsmart/* @rytilahti homeassistant/components/esphome/* @OttoWinter @jesserockz -homeassistant/components/essent/* @TheLastProject homeassistant/components/evohome/* @zxdavb homeassistant/components/ezviz/* @RenierM26 @baqs homeassistant/components/faa_delays/* @ntilley905 @@ -174,6 +173,7 @@ homeassistant/components/flo/* @dmulcahey homeassistant/components/flock/* @fabaff homeassistant/components/flume/* @ChrisMandich @bdraco homeassistant/components/flunearyou/* @bachya +homeassistant/components/flux_led/* @icemanch homeassistant/components/forecast_solar/* @klaasnicolaas @frenck homeassistant/components/forked_daapd/* @uvjustin homeassistant/components/fortios/* @kimfrellsen @@ -181,7 +181,7 @@ 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/fritzbox/* @mib1185 @flabbamann homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garages_amsterdam/* @klaasnicolaas @@ -227,9 +227,8 @@ homeassistant/components/homematic/* @pvizeli @danielperna84 homeassistant/components/honeywell/* @rdfurman homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop @fphammerle -homeassistant/components/huawei_router/* @abmantis homeassistant/components/hue/* @balloob @frenck -homeassistant/components/huisbaasje/* @denniss17 +homeassistant/components/huisbaasje/* @dennisschroer homeassistant/components/humidifier/* @home-assistant/core @Shulyaka homeassistant/components/hunterdouglas_powerview/* @bdraco homeassistant/components/hvv_departures/* @vigonotion @@ -287,6 +286,7 @@ homeassistant/components/litterrobot/* @natekspencer homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd +homeassistant/components/lookin/* @ANMalko homeassistant/components/loopenergy/* @pavoni homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @mzdrale @@ -336,7 +336,6 @@ homeassistant/components/nam/* @bieniu homeassistant/components/nanoleaf/* @milanmeu homeassistant/components/neato/* @dshokouhi @Santobert homeassistant/components/nederlandse_spoorwegen/* @YarmoM -homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @allenporter homeassistant/components/netatmo/* @cgtobi @@ -361,10 +360,11 @@ homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuki/* @pschmitt @pvizeli @pree homeassistant/components/numato/* @clssn homeassistant/components/number/* @home-assistant/core @Shulyaka -homeassistant/components/nut/* @bdraco +homeassistant/components/nut/* @bdraco @ollo69 homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi +homeassistant/components/octoprint/* @rfleming71 homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/ombi/* @larssont homeassistant/components/omnilogic/* @oliver84 @djtimca @gentoosu @@ -423,6 +423,7 @@ homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff homeassistant/components/recollect_waste/* @bachya +homeassistant/components/recorder/* @home-assistant/core homeassistant/components/rejseplanen/* @DarkFox homeassistant/components/renault/* @epenet homeassistant/components/repetier/* @MTrab @@ -497,7 +498,7 @@ homeassistant/components/srp_energy/* @briglx homeassistant/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm -homeassistant/components/stookalert/* @fwestenberg +homeassistant/components/stookalert/* @fwestenberg @frenck homeassistant/components/stream/* @hunterjm @uvjustin @allenporter homeassistant/components/stt/* @pvizeli homeassistant/components/subaru/* @G-Two @@ -533,7 +534,6 @@ homeassistant/components/tile/* @bachya homeassistant/components/time_date/* @fabaff homeassistant/components/tmb/* @alemuro homeassistant/components/todoist/* @boralyl -homeassistant/components/toon/* @frenck homeassistant/components/totalconnect/* @austinmroczek homeassistant/components/tplink/* @rytilahti @thegardenmonkey homeassistant/components/traccar/* @ludeeus @@ -544,7 +544,7 @@ homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force homeassistant/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/tts/* @pvizeli -homeassistant/components/tuya/* @Tuya @zlinoliver @METISU +homeassistant/components/tuya/* @Tuya @zlinoliver @METISU @frenck homeassistant/components/twentemilieu/* @frenck homeassistant/components/twinkly/* @dr1rrb homeassistant/components/ubus/* @noltari @@ -562,6 +562,7 @@ homeassistant/components/utility_meter/* @dgomes homeassistant/components/vallox/* @andre-richter homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velux/* @Julius2342 +homeassistant/components/venstar/* @garbled1 homeassistant/components/vera/* @pavoni homeassistant/components/verisure/* @frenck homeassistant/components/versasense/* @flamm3blemuff1n @@ -571,7 +572,7 @@ homeassistant/components/vicare/* @oischinger homeassistant/components/vilfo/* @ManneW homeassistant/components/vivotek/* @HarlemSquirrel homeassistant/components/vizio/* @raman325 -homeassistant/components/vlc_telnet/* @rodripf @dmcc +homeassistant/components/vlc_telnet/* @rodripf @dmcc @MartinHjelmare homeassistant/components/volkszaehler/* @fabaff homeassistant/components/volumio/* @OnFreund homeassistant/components/wake_on_lan/* @ntilley905 diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index c528aff221f..abd5ddc71d5 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -9,7 +9,7 @@ from typing import Any, Dict, Mapping, Optional, Tuple, cast import jwt from homeassistant import data_entry_flow -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.util import dt as dt_util @@ -155,6 +155,7 @@ class AuthManager: self._providers = providers self._mfa_modules = mfa_modules self.login_flow = AuthManagerFlowManager(hass, self) + self._revoke_callbacks: dict[str, list[CALLBACK_TYPE]] = {} @property def auth_providers(self) -> list[AuthProvider]: @@ -275,6 +276,12 @@ class AuthManager: self, user: models.User, credentials: models.Credentials ) -> None: """Link credentials to an existing user.""" + linked_user = await self.async_get_user_by_credentials(credentials) + if linked_user == user: + return + if linked_user is not None: + raise ValueError("Credential is already linked to a user") + await self._store.async_link_user(user, credentials) async def async_remove_user(self, user: models.User) -> None: @@ -285,7 +292,7 @@ class AuthManager: ] if tasks: - await asyncio.wait(tasks) + await asyncio.gather(*tasks) await self._store.async_remove_user(user) @@ -446,6 +453,28 @@ class AuthManager: """Delete a refresh token.""" await self._store.async_remove_refresh_token(refresh_token) + callbacks = self._revoke_callbacks.pop(refresh_token.id, []) + for revoke_callback in callbacks: + revoke_callback() + + @callback + def async_register_revoke_token_callback( + self, refresh_token_id: str, revoke_callback: CALLBACK_TYPE + ) -> CALLBACK_TYPE: + """Register a callback to be called when the refresh token id is revoked.""" + if refresh_token_id not in self._revoke_callbacks: + self._revoke_callbacks[refresh_token_id] = [] + + callbacks = self._revoke_callbacks[refresh_token_id] + callbacks.append(revoke_callback) + + @callback + def unregister() -> None: + if revoke_callback in callbacks: + callbacks.remove(revoke_callback) + + return unregister + @callback def async_create_access_token( self, refresh_token: models.RefreshToken, remote_ip: str | None = None diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index ec5d5b7cd03..0f53ddc900d 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -25,7 +25,7 @@ from . import ( SetupFlow, ) -REQUIREMENTS = ["pyotp==2.3.0"] +REQUIREMENTS = ["pyotp==2.6.0"] CONF_MESSAGE = "message" @@ -56,10 +56,10 @@ def _generate_secret() -> str: def _generate_random() -> int: - """Generate a 8 digit number.""" + """Generate a 32 digit number.""" import pyotp # pylint: disable=import-outside-toplevel - return int(pyotp.random_base32(length=8, chars=list("1234567890"))) + return int(pyotp.random_base32(length=32, chars=list("1234567890"))) def _generate_otp(secret: str, count: int) -> str: @@ -245,8 +245,7 @@ class NotifyAuthModule(MultiFactorAuthModule): await self._async_load() assert self._user_settings is not None - notify_setting = self._user_settings.get(user_id) - if notify_setting is None: + if (notify_setting := self._user_settings.get(user_id)) is None: _LOGGER.error("Cannot find user %s", user_id) return diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 0ff7e1147b1..e0e2b9522d9 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -18,7 +18,7 @@ from . import ( SetupFlow, ) -REQUIREMENTS = ["pyotp==2.3.0", "PyQRCode==1.2.1"] +REQUIREMENTS = ["pyotp==2.6.0", "PyQRCode==1.2.1"] CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA) @@ -181,7 +181,7 @@ class TotpSetupFlow(SetupFlow): # to fix typing complaint self._auth_module: TotpAuthModule = auth_module self._user = user - self._ota_secret: str | None = None + self._ota_secret: str = "" self._url = None # type Optional[str] self._image = None # type Optional[str] diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 694ea2b7379..101c331b842 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable -import logging from typing import Any import voluptuous as vol @@ -16,8 +15,6 @@ from .util import test_all POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA}) -_LOGGER = logging.getLogger(__name__) - class AbstractPermissions: """Default permissions class.""" diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index f2b3d5e6ec4..f111ff6a079 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -17,7 +17,10 @@ import yarl from homeassistant import config as conf_util, config_entries, core, loader from homeassistant.components import http -from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER +from homeassistant.const import ( + REQUIRED_NEXT_PYTHON_HA_RELEASE, + REQUIRED_NEXT_PYTHON_VER, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import area_registry, device_registry, entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -240,11 +243,14 @@ async def async_from_config_dict( stop = monotonic() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) - if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER: + if ( + REQUIRED_NEXT_PYTHON_HA_RELEASE + and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER + ): msg = ( "Support for the running Python version " f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will " - f"be removed in the first release after {REQUIRED_NEXT_PYTHON_DATE}. " + f"be removed in Home Assistant {REQUIRED_NEXT_PYTHON_HA_RELEASE}. " "Please upgrade Python to " f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER)} or " "higher." diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index be474661eac..3407d387169 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ATTRIBUTION, DEFAULT_CACHEDB, DOMAIN, LOGGER @@ -322,14 +322,14 @@ class AbodeDevice(AbodeEntity): } @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "manufacturer": "Abode", - "name": self._device.name, - "device_type": self._device.type, - } + return DeviceInfo( + identifiers={(DOMAIN, self._device.device_id)}, + manufacturer="Abode", + model=self._device.type, + name=self._device.name, + ) def _update_callback(self, device): """Update the device state.""" diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index 8b2f622d6e7..0c22766e373 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -1,4 +1,6 @@ """Config flow for the Abode Security System component.""" +from http import HTTPStatus + from abodepy import Abode from abodepy.exceptions import AbodeAuthenticationException, AbodeException from abodepy.helpers.errors import MFA_CODE_REQUIRED @@ -6,7 +8,7 @@ from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_BAD_REQUEST +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER @@ -51,7 +53,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.error("Unable to connect to Abode: %s", ex) - if ex.errcode == HTTP_BAD_REQUEST: + if ex.errcode == HTTPStatus.BAD_REQUEST: errors = {"base": "invalid_auth"} else: diff --git a/homeassistant/components/abode/translations/bg.json b/homeassistant/components/abode/translations/bg.json index 285bf18d330..955ed18c82c 100644 --- a/homeassistant/components/abode/translations/bg.json +++ b/homeassistant/components/abode/translations/bg.json @@ -1,9 +1,19 @@ { "config": { "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Abode." }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "Email" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/abode/translations/hu.json b/homeassistant/components/abode/translations/hu.json index a4ce211d21a..8f835cbbe2d 100644 --- a/homeassistant/components/abode/translations/hu.json +++ b/homeassistant/components/abode/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index 04b1b4b39c6..fd391a81bad 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -2,7 +2,7 @@ "domain": "accuweather", "name": "AccuWeather", "documentation": "https://www.home-assistant.io/integrations/accuweather/", - "requirements": ["accuweather==0.2.0"], + "requirements": ["accuweather==0.3.0"], "codeowners": ["@bieniu"], "config_flow": true, "quality_scale": "platinum", diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index b5f979b45cf..3159293a4cc 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -5,8 +5,9 @@ from typing import Any, cast from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TEMPERATURE +from homeassistant.const import CONF_NAME, DEVICE_CLASS_TEMPERATURE from homeassistant.core import HomeAssistant, callback +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 @@ -59,6 +60,7 @@ async def async_setup_entry( class AccuWeatherSensor(CoordinatorEntity, SensorEntity): """Define an AccuWeather entity.""" + _attr_attribution = ATTRIBUTION coordinator: AccuWeatherDataUpdateCoordinator entity_description: AccuWeatherSensorDescription @@ -75,7 +77,7 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): self._sensor_data = _get_sensor_data( coordinator.data, forecast_day, description.key ) - self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attrs: dict[str, Any] = {} if forecast_day is not None: self._attr_name = f"{name} {description.name} {forecast_day}d" self._attr_unique_id = ( @@ -92,12 +94,12 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): else: self._unit_system = API_IMPERIAL self._attr_native_unit_of_measurement = description.unit_imperial - self._attr_device_info = { - "identifiers": {(DOMAIN, coordinator.location_key)}, - "name": NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, coordinator.location_key)}, + manufacturer=MANUFACTURER, + name=NAME, + ) self.forecast_day = forecast_day @property diff --git a/homeassistant/components/accuweather/translations/bg.json b/homeassistant/components/accuweather/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/accuweather/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json index eb3729fd2c4..4ebb296d5d3 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -16,7 +16,7 @@ "longitude": "\u7d93\u5ea6", "name": "\u540d\u7a31" }, - "description": "\u5047\u5982\u4f60\u9700\u8981\u5354\u52a9\u9032\u884c\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/accuweather/\n\n\u67d0\u4e9b\u50b3\u611f\u5668\u9810\u8a2d\u70ba\u672a\u555f\u7528\uff0c\u53ef\u4ee5\u65bc\u6574\u5408\u8a2d\u5b9a\u4e2d\u555f\u7528\u9019\u4e9b\u5be6\u9ad4\u3002\u5929\u6c23\u9810\u5831\u9810\u8a2d\u672a\u958b\u555f\u3002\u53ef\u4ee5\u65bc\u6574\u5408\u9078\u9805\u4e2d\u958b\u555f\u3002", + "description": "\u5047\u5982\u4f60\u9700\u8981\u5354\u52a9\u9032\u884c\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/accuweather/\n\n\u67d0\u4e9b\u611f\u6e2c\u5668\u9810\u8a2d\u70ba\u672a\u555f\u7528\uff0c\u53ef\u4ee5\u65bc\u6574\u5408\u8a2d\u5b9a\u4e2d\u555f\u7528\u9019\u4e9b\u5be6\u9ad4\u3002\u5929\u6c23\u9810\u5831\u9810\u8a2d\u672a\u958b\u555f\u3002\u53ef\u4ee5\u65bc\u6574\u5408\u9078\u9805\u4e2d\u958b\u555f\u3002", "title": "AccuWeather" } } diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index cd8d64cc80f..af2d1c15b2b 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -19,6 +19,7 @@ from homeassistant.components.weather import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utc_from_timestamp @@ -66,12 +67,12 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): TEMP_CELSIUS if coordinator.is_metric else TEMP_FAHRENHEIT ) self._attr_attribution = ATTRIBUTION - self._attr_device_info = { - "identifiers": {(DOMAIN, coordinator.location_key)}, - "name": NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, coordinator.location_key)}, + manufacturer=MANUFACTURER, + name=NAME, + ) @property def condition(self) -> str | None: diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 947b774b7bd..747c7c98d73 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -129,8 +129,7 @@ class AcerSwitch(SwitchEntity): self._attr_available = False for key in self._attributes: - msg = CMD_DICT.get(key) - if msg: + if msg := CMD_DICT.get(key): awns = self._write_read_format(msg) self._attributes[key] = awns self._attr_extra_state_attributes = self._attributes diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py index 459f4ab2097..3338bf9667d 100644 --- a/homeassistant/components/acmeda/base.py +++ b/homeassistant/components/acmeda/base.py @@ -77,11 +77,11 @@ class AcmedaBase(entity.Entity): return self.roller.name @property - def device_info(self): + def device_info(self) -> entity.DeviceInfo: """Return the device info.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.roller.name, - "manufacturer": "Rollease Acmeda", - "via_device": (DOMAIN, self.roller.hub.id), - } + return entity.DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Rollease Acmeda", + name=self.roller.name, + via_device=(DOMAIN, self.roller.hub.id), + ) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 1abd83fdbfc..783c2a9f2f8 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -1,7 +1,6 @@ """Support for Adax wifi-enabled home heaters.""" from __future__ import annotations -import logging from typing import Any from adax import Adax @@ -21,11 +20,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ACCOUNT_ID - -_LOGGER = logging.getLogger(__name__) +from .const import ACCOUNT_ID, DOMAIN async def async_setup_entry( @@ -41,8 +39,11 @@ async def async_setup_entry( ) async_add_entities( - AdaxDevice(room, adax_data_handler) - for room in await adax_data_handler.get_rooms() + ( + AdaxDevice(room, adax_data_handler) + for room in await adax_data_handler.get_rooms() + ), + True, ) @@ -58,69 +59,51 @@ class AdaxDevice(ClimateEntity): def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None: """Initialize the heater.""" - self._heater_data = heater_data + self._device_id = heater_data["id"] self._adax_data_handler = adax_data_handler self._attr_unique_id = f"{heater_data['homeId']}_{heater_data['id']}" - - @property - def name(self) -> str: - """Return the name of the device, if any.""" - return self._heater_data["name"] - - @property - def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode.""" - if self._heater_data["heatingEnabled"]: - return HVAC_MODE_HEAT - return HVAC_MODE_OFF - - @property - def icon(self) -> str: - """Return nice icon for heater.""" - if self.hvac_mode == HVAC_MODE_HEAT: - return "mdi:radiator" - return "mdi:radiator-off" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, heater_data["id"])}, + name=self.name, + manufacturer="Adax", + ) async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set hvac mode.""" if hvac_mode == HVAC_MODE_HEAT: - temperature = max( - self.min_temp, self._heater_data.get("targetTemperature", self.min_temp) - ) + temperature = max(self.min_temp, self.target_temperature or self.min_temp) await self._adax_data_handler.set_room_target_temperature( - self._heater_data["id"], temperature, True + self._device_id, temperature, True ) elif hvac_mode == HVAC_MODE_OFF: await self._adax_data_handler.set_room_target_temperature( - self._heater_data["id"], self.min_temp, False + self._device_id, self.min_temp, False ) else: return await self._adax_data_handler.update() - @property - def current_temperature(self) -> float | None: - """Return the current temperature.""" - return self._heater_data.get("temperature") - - @property - def target_temperature(self) -> int | None: - """Return the temperature we try to reach.""" - return self._heater_data.get("targetTemperature") - async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return await self._adax_data_handler.set_room_target_temperature( - self._heater_data["id"], temperature, True + self._device_id, temperature, True ) async def async_update(self) -> None: """Get the latest data.""" for room in await self._adax_data_handler.get_rooms(): - if room["id"] == self._heater_data["id"]: - self._heater_data = room - return + if room["id"] != self._device_id: + continue + self._attr_name = room["name"] + self._attr_current_temperature = room.get("temperature") + self._attr_target_temperature = room.get("targetTemperature") + if room["heatingEnabled"]: + self._attr_hvac_mode = HVAC_MODE_HEAT + self._attr_icon = "mdi:radiator" + else: + self._attr_hvac_mode = HVAC_MODE_OFF + self._attr_icon = "mdi:radiator-off" + return diff --git a/homeassistant/components/adax/translations/bg.json b/homeassistant/components/adax/translations/bg.json new file mode 100644 index 00000000000..329b8fd8399 --- /dev/null +++ b/homeassistant/components/adax/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index eedc1fe4b03..d76d32ce066 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -196,14 +196,14 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this AdGuard Home instance.""" - return { - "identifiers": { + return DeviceInfo( + entry_type="service", + identifiers={ (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore }, - "name": "AdGuard Home", - "manufacturer": "AdGuard Team", - "sw_version": self.hass.data[DOMAIN][self._entry.entry_id].get( + manufacturer="AdGuard Team", + name="AdGuard Home", + sw_version=self.hass.data[DOMAIN][self._entry.entry_id].get( DATA_ADGUARD_VERSION ), - "entry_type": "service", - } + ) diff --git a/homeassistant/components/adguard/translations/bg.json b/homeassistant/components/adguard/translations/bg.json index 97d8547861f..2eb6a211ecd 100644 --- a/homeassistant/components/adguard/translations/bg.json +++ b/homeassistant/components/adguard/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "hassio_confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 AdGuard Home, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430: {addon} ?", @@ -10,7 +13,9 @@ }, "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", "ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "verify_ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0430\u0434\u0435\u0436\u0434\u0435\u043d \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index ad3a95123c7..4af0b3e5f60 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -12,7 +12,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ADVANTAGE_AIR_RETRY, DOMAIN ADVANTAGE_AIR_SYNC_INTERVAL = 15 -PLATFORMS = ["climate", "cover", "binary_sensor", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "climate", "cover", "select", "sensor", "switch"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index 6a050e4086a..eec0ce7dfa5 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, BinarySensorEntity, ) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN from .entity import AdvantageAirEntity @@ -34,6 +35,7 @@ class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Filter.""" _attr_device_class = DEVICE_CLASS_PROBLEM + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key): """Initialize an Advantage Air Filter.""" @@ -65,13 +67,14 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): @property def is_on(self): """Return if motion is detect.""" - return self._zone["motion"] + return self._zone["motion"] == 20 class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Zone MyZone.""" _attr_entity_registry_enabled_default = False + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone MyZone.""" diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index ffb75e78c01..9514cc7915b 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -1,5 +1,6 @@ """Advantage Air parent entity class.""" +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -14,13 +15,13 @@ class AdvantageAirEntity(CoordinatorEntity): self.async_change = instance["async_change"] self.ac_key = ac_key self.zone_key = zone_key - self._attr_device_info = { - "identifiers": {(DOMAIN, self.coordinator.data["system"]["rid"])}, - "name": self.coordinator.data["system"]["name"], - "manufacturer": "Advantage Air", - "model": self.coordinator.data["system"]["sysType"], - "sw_version": self.coordinator.data["system"]["myAppRev"], - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.coordinator.data["system"]["rid"])}, + manufacturer="Advantage Air", + model=self.coordinator.data["system"]["sysType"], + name=self.coordinator.data["system"]["name"], + sw_version=self.coordinator.data["system"]["myAppRev"], + ) @property def _ac(self): diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py new file mode 100644 index 00000000000..79d23f8dfd1 --- /dev/null +++ b/homeassistant/components/advantage_air/select.py @@ -0,0 +1,53 @@ +"""Select platform for Advantage Air integration.""" + +from homeassistant.components.select import SelectEntity + +from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN +from .entity import AdvantageAirEntity + +ADVANTAGE_AIR_INACTIVE = "Inactive" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AdvantageAir toggle platform.""" + + instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] + + entities = [] + for ac_key in instance["coordinator"].data["aircons"]: + entities.append(AdvantageAirMyZone(instance, ac_key)) + async_add_entities(entities) + + +class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): + """Representation of Advantage Air MyZone control.""" + + _attr_icon = "mdi:home-thermometer" + _attr_options = [ADVANTAGE_AIR_INACTIVE] + _number_to_name = {0: ADVANTAGE_AIR_INACTIVE} + _name_to_number = {ADVANTAGE_AIR_INACTIVE: 0} + + def __init__(self, instance, ac_key): + """Initialize an Advantage Air MyZone control.""" + super().__init__(instance, ac_key) + self._attr_name = f'{self._ac["name"]} MyZone' + self._attr_unique_id = ( + f'{self.coordinator.data["system"]["rid"]}-{ac_key}-myzone' + ) + + for zone in instance["coordinator"].data["aircons"][ac_key]["zones"].values(): + if zone["type"] > 0: + self._name_to_number[zone["name"]] = zone["number"] + self._number_to_name[zone["number"]] = zone["name"] + self._attr_options.append(zone["name"]) + + @property + def current_option(self): + """Return the fresh air status.""" + return self._number_to_name[self._ac["myZone"]] + + async def async_select_option(self, option): + """Set the MyZone.""" + await self.async_change( + {self.ac_key: {"info": {"myZone": self._name_to_number[option]}}} + ) diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 4f3258e824e..d879693fdb5 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -6,7 +6,7 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv, entity_platform from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN @@ -50,6 +50,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air timer control.""" _attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, action): """Initialize the Advantage Air timer control.""" @@ -84,6 +85,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Vent Sensor.""" @@ -113,6 +115,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone wireless signal sensor.""" @@ -148,6 +151,7 @@ class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_state_class = STATE_CLASS_MEASUREMENT _attr_entity_registry_enabled_default = False + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Temp Sensor.""" diff --git a/homeassistant/components/advantage_air/translations/bg.json b/homeassistant/components/advantage_air/translations/bg.json new file mode 100644 index 00000000000..2293e4d4c53 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 7a20e77f0b0..a914a23a0da 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -52,7 +52,7 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: 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.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index e84060b444d..ba37c66da64 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -1,7 +1,10 @@ """Constant values for the AEMET OpenData component.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -252,12 +255,14 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_RAIN, @@ -268,6 +273,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_RAIN_PROB, name="Rain probability", native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_SNOW, @@ -278,6 +284,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_SNOW_PROB, name="Snow probability", native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_STATION_ID, @@ -296,18 +303,21 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_STORM_PROB, name="Storm probability", native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_TEMPERATURE_FEELING, name="Temperature feeling", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_TOWN_ID, @@ -326,6 +336,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_WIND_BEARING, name="Wind bearing", native_unit_of_measurement=DEGREE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_WIND_MAX_SPEED, @@ -336,6 +347,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + state_class=STATE_CLASS_MEASUREMENT, ), ) diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index a3b41f8314c..be3fd74d6bd 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -1,6 +1,7 @@ """Support for non-delivered packages recorded in AfterShip.""" from __future__ import annotations +from http import HTTPStatus import logging from typing import Any, Final @@ -11,7 +12,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, SensorEntity, ) -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, HTTP_OK +from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -64,7 +65,7 @@ async def async_setup_platform( await aftership.get_trackings() - if not aftership.meta or aftership.meta["code"] != HTTP_OK: + if not aftership.meta or aftership.meta["code"] != HTTPStatus.OK: _LOGGER.error( "No tracking data found. Check API key is correct: %s", aftership.meta ) @@ -109,6 +110,7 @@ async def async_setup_platform( class AfterShipSensor(SensorEntity): """Representation of a AfterShip sensor.""" + _attr_attribution = ATTRIBUTION _attr_native_unit_of_measurement: str = "packages" _attr_icon: str = ICON @@ -150,7 +152,7 @@ class AfterShipSensor(SensorEntity): if not self.aftership.meta: _LOGGER.error("Unknown errors when querying") return - if self.aftership.meta["code"] != HTTP_OK: + if self.aftership.meta["code"] != HTTPStatus.OK: _LOGGER.error( "Errors when querying AfterShip. %s", str(self.aftership.meta) ) @@ -191,7 +193,6 @@ class AfterShipSensor(SensorEntity): _LOGGER.debug("Ignoring %s as it has status: %s", name, status) self._attributes = { - ATTR_ATTRIBUTION: ATTRIBUTION, **status_counts, ATTR_TRACKINGS: trackings, } diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py index 8f139af8963..572f80a138f 100644 --- a/homeassistant/components/agent_dvr/alarm_control_panel.py +++ b/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -11,6 +11,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, ) +from homeassistant.helpers.entity import DeviceInfo from .const import CONNECTION, DOMAIN as AGENT_DOMAIN @@ -45,12 +46,12 @@ class AgentBaseStation(AlarmControlPanelEntity): self._client = client self._attr_name = f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}" self._attr_unique_id = f"{client.unique}_CP" - self._attr_device_info = { - "identifiers": {(AGENT_DOMAIN, client.unique)}, - "manufacturer": "Agent", - "model": CONST_ALARM_CONTROL_PANEL_NAME, - "sw_version": client.version, - } + self._attr_device_info = DeviceInfo( + identifiers={(AGENT_DOMAIN, client.unique)}, + manufacturer="Agent", + model=CONST_ALARM_CONTROL_PANEL_NAME, + sw_version=client.version, + ) async def async_update(self): """Update the state of the device.""" diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 8a29428a833..474d1f08b80 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -13,6 +13,7 @@ from homeassistant.components.mjpeg.camera import ( ) from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.helpers import entity_platform +from homeassistant.helpers.entity import DeviceInfo from .const import ( ATTRIBUTION, @@ -79,13 +80,13 @@ class AgentCamera(MjpegCamera): self._attr_name = f"{device.client.name} {device.name}" self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}" super().__init__(device_info) - self._attr_device_info = { - "identifiers": {(AGENT_DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Agent", - "model": "Camera", - "sw_version": device.client.version, - } + self._attr_device_info = DeviceInfo( + identifiers={(AGENT_DOMAIN, self.unique_id)}, + manufacturer="Agent", + model="Camera", + name=self.name, + sw_version=device.client.version, + ) async def async_update(self): """Update our state from the Agent API.""" diff --git a/homeassistant/components/agent_dvr/config_flow.py b/homeassistant/components/agent_dvr/config_flow.py index a21e6855337..7dd3c7d5bc3 100644 --- a/homeassistant/components/agent_dvr/config_flow.py +++ b/homeassistant/components/agent_dvr/config_flow.py @@ -33,9 +33,7 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: await agent_client.update() - except AgentConnectionError: - pass - except AgentError: + except (AgentConnectionError, AgentError): pass await agent_client.close() diff --git a/homeassistant/components/agent_dvr/translations/bg.json b/homeassistant/components/agent_dvr/translations/bg.json new file mode 100644 index 00000000000..527adb67bf7 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/hu.json b/homeassistant/components/agent_dvr/translations/hu.json index b8fec1c281d..83751d72eaf 100644 --- a/homeassistant/components/agent_dvr/translations/hu.json +++ b/homeassistant/components/agent_dvr/translations/hu.json @@ -4,7 +4,7 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 1b8ab5f9c30..1e38bad55a8 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -6,10 +6,7 @@ import logging from typing import Final, final from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, -) +from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -41,7 +38,6 @@ SCAN_INTERVAL: Final = timedelta(seconds=30) PROP_TO_ATTR: Final[dict[str, str]] = { "air_quality_index": ATTR_AQI, - "attribution": ATTR_ATTRIBUTION, "carbon_dioxide": ATTR_CO2, "carbon_monoxide": ATTR_CO, "nitrogen_oxide": ATTR_N2O, @@ -114,11 +110,6 @@ class AirQualityEntity(Entity): """Return the CO2 (carbon dioxide) level.""" return None - @property - def attribution(self) -> StateType: - """Return the attribution.""" - return None - @property def sulphur_dioxide(self) -> StateType: """Return the SO2 (sulphur dioxide) level.""" diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 598aa15b9b6..a6fa9f2d1d6 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -1,6 +1,7 @@ """Adds config flow for Airly.""" from __future__ import annotations +from http import HTTPStatus from typing import Any from aiohttp import ClientSession @@ -10,14 +11,7 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - HTTP_NOT_FOUND, - HTTP_UNAUTHORIZED, -) +from homeassistant.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 @@ -60,9 +54,9 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): use_nearest=True, ) except AirlyError as err: - if err.status_code == HTTP_UNAUTHORIZED: + if err.status_code == HTTPStatus.UNAUTHORIZED: errors["base"] = "invalid_api_key" - if err.status_code == HTTP_NOT_FOUND: + if err.status_code == HTTPStatus.NOT_FOUND: errors["base"] = "wrong_location" else: if not location_point_valid: diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index c583a56c22b..801bca58412 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -32,3 +32,4 @@ MANUFACTURER: Final = "Airly sp. z o.o." MAX_UPDATE_INTERVAL: Final = 90 MIN_UPDATE_INTERVAL: Final = 5 NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." +URL = "https://airly.org/map/#{latitude},{longitude}" diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index fc587f15140..76b4e7d9d48 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -27,6 +27,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -54,6 +55,7 @@ from .const import ( MANUFACTURER, SUFFIX_LIMIT, SUFFIX_PERCENT, + URL, ) PARALLEL_UPDATES = 1 @@ -151,14 +153,15 @@ class AirlySensor(CoordinatorEntity, SensorEntity): ) -> None: """Initialize.""" super().__init__(coordinator) - self._attr_device_info = { - "identifiers": { - (DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}") - }, - "name": DEFAULT_NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}")}, + manufacturer=MANUFACTURER, + name=DEFAULT_NAME, + configuration_url=URL.format( + latitude=coordinator.latitude, longitude=coordinator.longitude + ), + ) self._attr_name = f"{name} {description.name}" self._attr_unique_id = ( f"{coordinator.latitude}-{coordinator.longitude}-{description.key}".lower() diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index 52ee1a0e8fc..be385d19645 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -64,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/airnow/translations/bg.json b/homeassistant/components/airnow/translations/bg.json index 5d274ec2b73..11928153a09 100644 --- a/homeassistant/components/airnow/translations/bg.json +++ b/homeassistant/components/airnow/translations/bg.json @@ -1,7 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "invalid_location": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0440\u0435\u0437\u0443\u043b\u0442\u0430\u0442\u0438 \u0437\u0430 \u0442\u043e\u0432\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/airthings/sensor.py b/homeassistant/components/airthings/sensor.py index 4aab2307d9a..b2960ff6066 100644 --- a/homeassistant/components/airthings/sensor.py +++ b/homeassistant/components/airthings/sensor.py @@ -22,12 +22,14 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, PRESSURE_MBAR, SIGNAL_STRENGTH_DECIBELS, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -64,6 +66,7 @@ SENSORS: dict[str, SensorEntityDescription] = { key="battery", device_class=DEVICE_CLASS_BATTERY, native_unit_of_measurement=PERCENTAGE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, name="Battery", ), "co2": SensorEntityDescription( @@ -96,6 +99,7 @@ SENSORS: dict[str, SensorEntityDescription] = { device_class=DEVICE_CLASS_SIGNAL_STRENGTH, name="RSSI", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "pm1": SensorEntityDescription( key="pm1", @@ -152,11 +156,12 @@ class AirthingsHeaterEnergySensor(CoordinatorEntity, SensorEntity): self._attr_name = f"{airthings_device.name} {entity_description.name}" self._attr_unique_id = f"{airthings_device.device_id}_{entity_description.key}" self._id = airthings_device.device_id - self._attr_device_info = { - "identifiers": {(DOMAIN, airthings_device.device_id)}, - "name": airthings_device.name, - "manufacturer": "Airthings", - } + self._attr_device_info = DeviceInfo( + configuration_url="https://dashboard.airthings.com/", + identifiers={(DOMAIN, airthings_device.device_id)}, + name=airthings_device.name, + manufacturer="Airthings", + ) @property def native_value(self) -> StateType: diff --git a/homeassistant/components/airthings/translations/bg.json b/homeassistant/components/airthings/translations/bg.json new file mode 100644 index 00000000000..df9d136dfe8 --- /dev/null +++ b/homeassistant/components/airthings/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "id": "ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/cs.json b/homeassistant/components/airthings/translations/cs.json new file mode 100644 index 00000000000..740a9675b96 --- /dev/null +++ b/homeassistant/components/airthings/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "id": "ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/pl.json b/homeassistant/components/airthings/translations/pl.json new file mode 100644 index 00000000000..08b4f80938a --- /dev/null +++ b/homeassistant/components/airthings/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "id": "ID", + "secret": "Sekret" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index 7202feb0527..5bac0c7c9a3 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -21,6 +21,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -96,14 +97,14 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): return super()._handle_coordinator_update() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info for this device.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Airtouch", - "model": "Airtouch 4", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + name=self.name, + manufacturer="Airtouch", + model="Airtouch 4", + ) @property def unique_id(self): @@ -211,14 +212,14 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): return super()._handle_coordinator_update() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info for this device.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Airtouch", - "model": "Airtouch 4", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Airtouch", + model="Airtouch 4", + name=self.name, + ) @property def unique_id(self): diff --git a/homeassistant/components/airtouch4/translations/bg.json b/homeassistant/components/airtouch4/translations/bg.json new file mode 100644 index 00000000000..4c9b4c409d0 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 0419e43cd81..72b063c9394 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta from math import ceil -from typing import Any +from typing import Any, Dict, cast from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( @@ -54,8 +54,6 @@ from .const import ( PLATFORMS = ["sensor"] -DATA_LISTENER = "listener" - DEFAULT_ATTRIBUTION = "Data provided by AirVisual" DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1) @@ -106,12 +104,12 @@ def async_get_cloud_coordinators_by_api_key( hass: HomeAssistant, api_key: str ) -> list[DataUpdateCoordinator]: """Get all DataUpdateCoordinator objects related to a particular API key.""" - coordinators = [] - for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items(): - config_entry = hass.config_entries.async_get_entry(entry_id) - if config_entry and config_entry.data.get(CONF_API_KEY) == api_key: - coordinators.append(coordinator) - return coordinators + return [ + attrs[DATA_COORDINATOR] + for entry_id, attrs in hass.data[DOMAIN].items() + if (entry := hass.config_entries.async_get_entry(entry_id)) + and entry.data.get(CONF_API_KEY) == api_key + ] @callback @@ -139,25 +137,25 @@ def async_sync_geo_coordinator_update_intervals( @callback def _standardize_geography_config_entry( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, entry: ConfigEntry ) -> None: """Ensure that geography config entries have appropriate properties.""" entry_updates = {} - if not config_entry.unique_id: + if not entry.unique_id: # If the config entry doesn't already have a unique ID, set one: - entry_updates["unique_id"] = config_entry.data[CONF_API_KEY] - if not config_entry.options: + entry_updates["unique_id"] = entry.data[CONF_API_KEY] + if not entry.options: # If the config entry doesn't already have any options set, set defaults: entry_updates["options"] = {CONF_SHOW_ON_MAP: True} - if config_entry.data.get(CONF_INTEGRATION_TYPE) not in [ + if entry.data.get(CONF_INTEGRATION_TYPE) not in [ INTEGRATION_TYPE_GEOGRAPHY_COORDS, INTEGRATION_TYPE_GEOGRAPHY_NAME, ]: # If the config entry data doesn't contain an integration type that we know # about, infer it from the data we have: - entry_updates["data"] = {**config_entry.data} - if CONF_CITY in config_entry.data: + entry_updates["data"] = {**entry.data} + if CONF_CITY in entry.data: entry_updates["data"][ CONF_INTEGRATION_TYPE ] = INTEGRATION_TYPE_GEOGRAPHY_NAME @@ -169,55 +167,55 @@ def _standardize_geography_config_entry( if not entry_updates: return - hass.config_entries.async_update_entry(config_entry, **entry_updates) + hass.config_entries.async_update_entry(entry, **entry_updates) @callback -def _standardize_node_pro_config_entry( - hass: HomeAssistant, config_entry: ConfigEntry -) -> None: +def _standardize_node_pro_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Ensure that Node/Pro config entries have appropriate properties.""" entry_updates: dict[str, Any] = {} - if CONF_INTEGRATION_TYPE not in config_entry.data: + if CONF_INTEGRATION_TYPE not in entry.data: # If the config entry data doesn't contain the integration type, add it: entry_updates["data"] = { - **config_entry.data, + **entry.data, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, } if not entry_updates: return - hass.config_entries.async_update_entry(config_entry, **entry_updates) + hass.config_entries.async_update_entry(entry, **entry_updates) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AirVisual as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_LISTENER: {}}) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} - if CONF_API_KEY in config_entry.data: - _standardize_geography_config_entry(hass, config_entry) + if CONF_API_KEY in entry.data: + _standardize_geography_config_entry(hass, entry) websession = aiohttp_client.async_get_clientsession(hass) - cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession) + cloud_api = CloudAPI(entry.data[CONF_API_KEY], session=websession) async def async_update_data() -> dict[str, Any]: """Get new data from the API.""" - if CONF_CITY in config_entry.data: + if CONF_CITY in entry.data: api_coro = cloud_api.air_quality.city( - config_entry.data[CONF_CITY], - config_entry.data[CONF_STATE], - config_entry.data[CONF_COUNTRY], + entry.data[CONF_CITY], + entry.data[CONF_STATE], + entry.data[CONF_COUNTRY], ) else: api_coro = cloud_api.air_quality.nearest_city( - config_entry.data[CONF_LATITUDE], - config_entry.data[CONF_LONGITUDE], + entry.data[CONF_LATITUDE], + entry.data[CONF_LONGITUDE], ) try: - return await api_coro + data = await api_coro + return cast(Dict[str, Any], data) except (InvalidKeyError, KeyExpiredError) as ex: raise ConfigEntryAuthFailed from ex except AirVisualError as err: @@ -226,7 +224,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b coordinator = DataUpdateCoordinator( hass, LOGGER, - name=async_get_geography_id(config_entry.data), + name=async_get_geography_id(entry.data), # We give a placeholder update interval in order to create the coordinator; # then, below, we use the coordinator's presence (along with any other # coordinators using the same API key) to calculate an actual, leveled @@ -236,16 +234,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) # Only geography-based entries have options: - hass.data[DOMAIN][DATA_LISTENER][ - config_entry.entry_id - ] = config_entry.add_update_listener(async_reload_entry) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) else: # Remove outdated air_quality entities from the entity registry if they exist: ent_reg = entity_registry.async_get(hass) for entity_entry in [ e for e in ent_reg.entities.values() - if e.config_entry_id == config_entry.entry_id + if e.config_entry_id == entry.entry_id and e.entity_id.startswith("air_quality") ]: LOGGER.debug( @@ -253,15 +249,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) ent_reg.async_remove(entity_entry.entity_id) - _standardize_node_pro_config_entry(hass, config_entry) + _standardize_node_pro_config_entry(hass, entry) async def async_update_data() -> dict[str, Any]: """Get new data from the API.""" try: async with NodeSamba( - config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD] + entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD] ) as node: - return await node.async_get_latest_measurements() + data = await node.async_get_latest_measurements() + return cast(Dict[str, Any], data) except NodeProError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err @@ -274,41 +271,38 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) await coordinator.async_config_entry_first_refresh() - - hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinator # Reassess the interval between 2 server requests - if CONF_API_KEY in config_entry.data: - async_sync_geo_coordinator_update_intervals( - hass, config_entry.data[CONF_API_KEY] - ) + if CONF_API_KEY in entry.data: + async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY]) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate an old config entry.""" - version = config_entry.version + version = entry.version LOGGER.debug("Migrating from version %s", version) # 1 -> 2: One geography per config entry if version == 1: - version = config_entry.version = 2 + version = entry.version = 2 # Update the config entry to only include the first geography (there is always # guaranteed to be at least one): - geographies = list(config_entry.data[CONF_GEOGRAPHIES]) + geographies = list(entry.data[CONF_GEOGRAPHIES]) first_geography = geographies.pop(0) first_id = async_get_geography_id(first_geography) hass.config_entries.async_update_entry( - config_entry, + entry, unique_id=first_id, title=f"Cloud API ({first_id})", - data={CONF_API_KEY: config_entry.data[CONF_API_KEY], **first_geography}, + data={CONF_API_KEY: entry.data[CONF_API_KEY], **first_geography}, ) # For any geographies that remain, create a new config entry for each one: @@ -321,7 +315,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data={CONF_API_KEY: config_entry.data[CONF_API_KEY], **geography}, + data={CONF_API_KEY: entry.data[CONF_API_KEY], **geography}, ) ) @@ -330,42 +324,39 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an AirVisual 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][DATA_COORDINATOR].pop(config_entry.entry_id) - remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) - remove_listener() - - if CONF_API_KEY in config_entry.data: + hass.data[DOMAIN].pop(entry.entry_id) + if CONF_API_KEY in entry.data: # Re-calculate the update interval period for any remaining consumers of # this API key: - async_sync_geo_coordinator_update_intervals( - hass, config_entry.data[CONF_API_KEY] - ) + async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY]) return unload_ok -async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle an options update.""" - await hass.config_entries.async_reload(config_entry.entry_id) + await hass.config_entries.async_reload(entry.entry_id) class AirVisualEntity(CoordinatorEntity): """Define a generic AirVisual entity.""" def __init__( - self, coordinator: DataUpdateCoordinator, description: EntityDescription + self, + coordinator: DataUpdateCoordinator, + entry: ConfigEntry, + description: EntityDescription, ) -> None: """Initialize.""" super().__init__(coordinator) self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._entry = entry self.entity_description = description async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 971dee161cb..636da54899f 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -262,9 +262,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class AirVisualOptionsFlowHandler(config_entries.OptionsFlow): """Handle an AirVisual options flow.""" - def __init__(self, config_entry: ConfigEntry) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize.""" - self.config_entry = config_entry + self.entry = entry async def async_step_init( self, user_input: dict[str, str] | None = None @@ -279,7 +279,7 @@ class AirVisualOptionsFlowHandler(config_entries.OptionsFlow): { vol.Required( CONF_SHOW_ON_MAP, - default=self.config_entry.options.get(CONF_SHOW_ON_MAP), + default=self.entry.options.get(CONF_SHOW_ON_MAP), ): bool } ), diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 486ef072f24..08896e13557 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -27,6 +27,7 @@ from homeassistant.const import ( DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_CELSIUS, ) @@ -103,6 +104,7 @@ NODE_PRO_SENSOR_DESCRIPTIONS = ( key=SENSOR_KIND_BATTERY_LEVEL, name="Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( @@ -189,26 +191,24 @@ POLLUTANT_UNITS = { async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up AirVisual sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] sensors: list[AirVisualGeographySensor | AirVisualNodeProSensor] - if config_entry.data[CONF_INTEGRATION_TYPE] in ( + if entry.data[CONF_INTEGRATION_TYPE] in ( INTEGRATION_TYPE_GEOGRAPHY_COORDS, INTEGRATION_TYPE_GEOGRAPHY_NAME, ): sensors = [ - AirVisualGeographySensor(coordinator, config_entry, description, locale) + AirVisualGeographySensor(coordinator, entry, description, locale) for locale in GEOGRAPHY_SENSOR_LOCALES for description in GEOGRAPHY_SENSOR_DESCRIPTIONS ] else: sensors = [ - AirVisualNodeProSensor(coordinator, description) + AirVisualNodeProSensor(coordinator, entry, description) for description in NODE_PRO_SENSOR_DESCRIPTIONS ] @@ -221,23 +221,22 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): def __init__( self, coordinator: DataUpdateCoordinator, - config_entry: ConfigEntry, + entry: ConfigEntry, description: SensorEntityDescription, locale: str, ) -> None: """Initialize.""" - super().__init__(coordinator, description) + super().__init__(coordinator, entry, description) self._attr_extra_state_attributes.update( { - ATTR_CITY: config_entry.data.get(CONF_CITY), - ATTR_STATE: config_entry.data.get(CONF_STATE), - ATTR_COUNTRY: config_entry.data.get(CONF_COUNTRY), + ATTR_CITY: entry.data.get(CONF_CITY), + ATTR_STATE: entry.data.get(CONF_STATE), + ATTR_COUNTRY: entry.data.get(CONF_COUNTRY), } ) self._attr_name = f"{GEOGRAPHY_SENSOR_LOCALES[locale]} {description.name}" - self._attr_unique_id = f"{config_entry.unique_id}_{locale}_{description.key}" - self._config_entry = config_entry + self._attr_unique_id = f"{entry.unique_id}_{locale}_{description.key}" self._locale = locale @property @@ -279,16 +278,16 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): # # We use any coordinates in the config entry and, in the case of a geography by # name, we fall back to the latitude longitude provided in the coordinator data: - latitude = self._config_entry.data.get( + latitude = self._entry.data.get( CONF_LATITUDE, self.coordinator.data["location"]["coordinates"][1], ) - longitude = self._config_entry.data.get( + longitude = self._entry.data.get( CONF_LONGITUDE, self.coordinator.data["location"]["coordinates"][0], ) - if self._config_entry.options[CONF_SHOW_ON_MAP]: + if self._entry.options[CONF_SHOW_ON_MAP]: self._attr_extra_state_attributes[ATTR_LATITUDE] = latitude self._attr_extra_state_attributes[ATTR_LONGITUDE] = longitude self._attr_extra_state_attributes.pop("lati", None) @@ -304,10 +303,13 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Define an AirVisual sensor related to a Node/Pro unit.""" def __init__( - self, coordinator: DataUpdateCoordinator, description: SensorEntityDescription + self, + coordinator: DataUpdateCoordinator, + entry: ConfigEntry, + description: SensorEntityDescription, ) -> None: """Initialize.""" - super().__init__(coordinator, description) + super().__init__(coordinator, entry, description) self._attr_name = ( f"{coordinator.data['settings']['node_name']} Node/Pro: {description.name}" @@ -317,16 +319,16 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): @property def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" - return { - "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])}, - "name": self.coordinator.data["settings"]["node_name"], - "manufacturer": "AirVisual", - "model": f'{self.coordinator.data["status"]["model"]}', - "sw_version": ( + return DeviceInfo( + identifiers={(DOMAIN, self.coordinator.data["serial_number"])}, + manufacturer="AirVisual", + model=f'{self.coordinator.data["status"]["model"]}', + name=self.coordinator.data["settings"]["node_name"], + sw_version=( f'Version {self.coordinator.data["status"]["system_version"]}' f'{self.coordinator.data["status"]["app_version"]}' ), - } + ) @callback def update_from_latest_data(self) -> None: diff --git a/homeassistant/components/airvisual/translations/bg.json b/homeassistant/components/airvisual/translations/bg.json index 7e463418576..b2c2b26bad3 100644 --- a/homeassistant/components/airvisual/translations/bg.json +++ b/homeassistant/components/airvisual/translations/bg.json @@ -1,11 +1,30 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "general_error": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" + }, "step": { "geography_by_name": { "data": { "city": "\u0413\u0440\u0430\u0434", "country": "\u0421\u0442\u0440\u0430\u043d\u0430" } + }, + "node_pro": { + "data": { + "ip_address": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + }, + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } } } } diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json index 48d4f5b98eb..681f32ca3bc 100644 --- a/homeassistant/components/airvisual/translations/hu.json +++ b/homeassistant/components/airvisual/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A hely m\u00e1r konfigur\u00e1lva van vagy a Node/Pro azonos\u00edt\u00f3 m\u00e1r regisztr\u00e1lva van.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/airvisual/translations/sensor.bg.json b/homeassistant/components/airvisual/translations/sensor.bg.json new file mode 100644 index 00000000000..311df560225 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.bg.json @@ -0,0 +1,9 @@ +{ + "state": { + "airvisual__pollutant_label": { + "o3": "\u041e\u0437\u043e\u043d", + "p1": "PM10", + "p2": "PM2.5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.zh-Hans.json b/homeassistant/components/airvisual/translations/sensor.zh-Hans.json new file mode 100644 index 00000000000..8c56f25246e --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.zh-Hans.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "\u4e00\u6c27\u5316\u78b3", + "n2": "\u4e8c\u6c27\u5316\u6c2e", + "o3": "\u81ed\u6c27", + "p1": "PM10", + "p2": "PM2.5", + "s2": "\u4e8c\u6c27\u5316\u786b" + }, + "airvisual__pollutant_level": { + "good": "\u826f\u597d", + "hazardous": "\u5371\u5bb3\u5065\u5eb7", + "moderate": "\u4e2d\u7b49", + "unhealthy": "\u4e0d\u5229\u4e8e\u5065\u5eb7", + "unhealthy_sensitive": "\u4e0d\u5229\u4e8e\u654f\u611f\u4eba\u7fa4", + "very_unhealthy": "\u975e\u5e38\u4e0d\u5229\u4e8e\u5065\u5eb7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py index 3fcc540d04b..ad992012c04 100644 --- a/homeassistant/components/alarm_control_panel/reproduce_state.py +++ b/homeassistant/components/alarm_control_panel/reproduce_state.py @@ -48,9 +48,7 @@ async def _async_reproduce_state( 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: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/alarm_control_panel/translations/he.json b/homeassistant/components/alarm_control_panel/translations/he.json index 836194caa80..9710be7c3c2 100644 --- a/homeassistant/components/alarm_control_panel/translations/he.json +++ b/homeassistant/components/alarm_control_panel/translations/he.json @@ -1,4 +1,30 @@ { + "device_automation": { + "action_type": { + "arm_away": "\u05d3\u05e8\u05d9\u05db\u05ea {entity_name} \u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", + "arm_home": "\u05d3\u05e8\u05d9\u05db\u05ea {entity_name} \u05d4\u05d1\u05d9\u05ea\u05d4", + "arm_night": "\u05d3\u05e8\u05d9\u05db\u05ea {entity_name} \u05dc\u05d9\u05dc\u05d4", + "arm_vacation": "\u05d3\u05e8\u05d9\u05db\u05ea {entity_name} \u05d7\u05d5\u05e4\u05e9\u05d4", + "disarm": "\u05e0\u05d9\u05d8\u05e8\u05d5\u05dc {entity_name}", + "trigger": "\u05d4\u05e4\u05e2\u05dc\u05ea {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} \u05d3\u05e8\u05d5\u05da \u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", + "is_armed_home": "{entity_name} \u05d3\u05e8\u05d5\u05da \u05d1\u05d9\u05ea", + "is_armed_night": "{entity_name} \u05d3\u05e8\u05d5\u05da \u05dc\u05d9\u05dc\u05d4", + "is_armed_vacation": "{entity_name} \u05d1\u05d7\u05d5\u05e4\u05e9\u05d4 \u05d3\u05e8\u05d5\u05db\u05d4", + "is_disarmed": "{entity_name} \u05de\u05e0\u05d5\u05d8\u05e8\u05dc", + "is_triggered": "{entity_name} \u05de\u05d5\u05e4\u05e2\u05dc" + }, + "trigger_type": { + "armed_away": "{entity_name} \u05d3\u05e8\u05d5\u05da \u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", + "armed_home": "{entity_name} \u05d3\u05e8\u05d5\u05da \u05d1\u05d1\u05d9\u05ea", + "armed_night": "{entity_name} \u05d3\u05e8\u05d5\u05da \u05dc\u05d9\u05dc\u05d4", + "armed_vacation": "{entity_name} \u05d7\u05d5\u05e4\u05e9\u05d4 \u05d3\u05e8\u05d5\u05db\u05d4", + "disarmed": "{entity_name} \u05de\u05e0\u05d5\u05d8\u05e8\u05dc", + "triggered": "{entity_name} \u05de\u05d5\u05e4\u05e2\u05dc" + } + }, "state": { "_": { "armed": "\u05d3\u05e8\u05d5\u05da", diff --git a/homeassistant/components/alarm_control_panel/translations/tr.json b/homeassistant/components/alarm_control_panel/translations/tr.json index cc509430436..c7a8235d5c9 100644 --- a/homeassistant/components/alarm_control_panel/translations/tr.json +++ b/homeassistant/components/alarm_control_panel/translations/tr.json @@ -1,23 +1,40 @@ { "device_automation": { + "action_type": { + "arm_away": "D\u0131\u015farda", + "arm_home": "Evde", + "arm_night": "Gece", + "disarm": "Devre d\u0131\u015f\u0131 {entity_name}", + "trigger": "Tetikle {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} D\u0131\u015farda Modu Aktif", + "is_armed_home": "{entity_name} Evde Modu Aktif", + "is_armed_night": "{entity_name} Gece Modu Aktif", + "is_disarmed": "{entity_name} Devre D\u0131\u015f\u0131", + "is_triggered": "{entity_name} tetiklendi" + }, "trigger_type": { - "disarmed": "{entity_name} b\u0131rak\u0131ld\u0131", + "armed_away": "{entity_name} D\u0131\u015farda Modu Aktif", + "armed_home": "{entity_name} Evde Modu Aktif", + "armed_night": "{entity_name} Gece Modu Aktif", + "disarmed": "{entity_name} Devre D\u0131\u015f\u0131", "triggered": "{entity_name} tetiklendi" } }, "state": { "_": { - "armed": "Etkin", - "armed_away": "Etkin d\u0131\u015far\u0131da", - "armed_custom_bypass": "Alarm etkin \u00f6zel baypas", - "armed_home": "Etkin evde", - "armed_night": "Etkin gece", + "armed": "Aktif", + "armed_away": "D\u0131\u015farda Aktif", + "armed_custom_bypass": "\u00d6zel Mod Aktif", + "armed_home": "Evde Aktif", + "armed_night": "Gece Aktif", "arming": "Alarm etkinle\u015fiyor", - "disarmed": "Etkisiz", + "disarmed": "Devre D\u0131\u015f\u0131", "disarming": "Alarm devre d\u0131\u015f\u0131", "pending": "Beklemede", "triggered": "Tetiklendi" } }, - "title": "Alarm kontrol paneli" + "title": "Alarm Kontrol Paneli" } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/zh-Hans.json b/homeassistant/components/alarm_control_panel/translations/zh-Hans.json index fa819e71b49..e955d21afdb 100644 --- a/homeassistant/components/alarm_control_panel/translations/zh-Hans.json +++ b/homeassistant/components/alarm_control_panel/translations/zh-Hans.json @@ -29,6 +29,7 @@ "armed_custom_bypass": "\u81ea\u5b9a\u4e49\u533a\u57df\u8b66\u6212", "armed_home": "\u5728\u5bb6\u8b66\u6212", "armed_night": "\u591c\u95f4\u8b66\u6212", + "armed_vacation": "\u5ea6\u5047\u8b66\u6212", "arming": "\u8b66\u6212\u4e2d", "disarmed": "\u8b66\u6212\u89e3\u9664", "disarming": "\u8b66\u6212\u89e3\u9664", diff --git a/homeassistant/components/alarmdecoder/translations/bg.json b/homeassistant/components/alarmdecoder/translations/bg.json new file mode 100644 index 00000000000..b918c0c7710 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "protocol": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + }, + "user": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/tr.json b/homeassistant/components/alarmdecoder/translations/tr.json index 276b733b31f..2334f9fb99f 100644 --- a/homeassistant/components/alarmdecoder/translations/tr.json +++ b/homeassistant/components/alarmdecoder/translations/tr.json @@ -34,7 +34,9 @@ "data": { "zone_name": "B\u00f6lge Ad\u0131", "zone_relayaddr": "R\u00f6le Adresi", - "zone_relaychan": "R\u00f6le Kanal\u0131" + "zone_relaychan": "R\u00f6le Kanal\u0131", + "zone_rfid": "RF Id", + "zone_type": "B\u00f6lge Tipi" } }, "zone_select": { diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 73be34e6d33..e72a1bcffa4 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -221,8 +221,7 @@ class Alert(ToggleEntity): async def watched_entity_change(self, ev): """Determine if the alert should start or stop.""" - to_state = ev.data.get("new_state") - if to_state is None: + if (to_state := ev.data.get("new_state")) is None: return _LOGGER.debug("Watched entity (%s) has changed", ev.data.get("entity_id")) if to_state.state == self._alert_state and not self._firing: diff --git a/homeassistant/components/alert/reproduce_state.py b/homeassistant/components/alert/reproduce_state.py index 9c8cbd19810..49658ab2495 100644 --- a/homeassistant/components/alert/reproduce_state.py +++ b/homeassistant/components/alert/reproduce_state.py @@ -30,9 +30,7 @@ async def _async_reproduce_state( 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: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 433b2929602..91729763804 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -1,13 +1,14 @@ """Support for Alexa skill auth.""" import asyncio from datetime import timedelta +from http import HTTPStatus import json import logging import aiohttp import async_timeout -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, HTTP_OK +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from homeassistant.util import dt @@ -119,7 +120,7 @@ class Auth: _LOGGER.debug("LWA response header: %s", response.headers) _LOGGER.debug("LWA response status: %s", response.status) - if response.status != HTTP_OK: + if response.status != HTTPStatus.OK: _LOGGER.error("Error calling LWA to get auth token") return None diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 46f421963ca..ea8a1ed8681 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -182,8 +182,7 @@ class AlexaCapability: """Serialize according to the Discovery API.""" result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} - instance = self.instance - if instance is not None: + if (instance := self.instance) is not None: result["instance"] = instance properties_supported = self.properties_supported() @@ -264,8 +263,7 @@ class AlexaCapability: "timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT), "uncertaintyInMilliseconds": 0, } - instance = self.instance - if instance is not None: + if (instance := self.instance) is not None: result["instance"] = instance yield result @@ -1098,8 +1096,7 @@ class AlexaThermostatController(AlexaCapability): supported_modes = [] hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES) for mode in hvac_modes: - thermostat_mode = API_THERMOSTAT_MODES.get(mode) - if thermostat_mode: + if thermostat_mode := API_THERMOSTAT_MODES.get(mode): supported_modes.append(thermostat_mode) preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES) @@ -1538,7 +1535,9 @@ class AlexaRangeController(AlexaCapability): labels=["Percentage", AlexaGlobalCatalog.SETTING_FAN_SPEED], min_value=0, max_value=100, - precision=percentage_step if percentage_step else 100, + # precision must be a divider of 100 and must be an integer; set step + # size to 1 for a consistent behavior except for on/off fans + precision=1 if percentage_step else 100, unit=AlexaGlobalCatalog.UNIT_PERCENT, ) return self._resource.serialize_capability_resources() diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 50463810bbf..1521afcae5a 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -1,11 +1,12 @@ """Support for Alexa skill service end point.""" import copy import hmac +from http import HTTPStatus import logging import uuid from homeassistant.components import http -from homeassistant.const import CONF_PASSWORD, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED +from homeassistant.const import CONF_PASSWORD from homeassistant.core import callback from homeassistant.helpers import template import homeassistant.util.dt as dt_util @@ -58,7 +59,7 @@ class AlexaFlashBriefingView(http.HomeAssistantView): if request.query.get(API_PASSWORD) is None: err = "No password provided for Alexa flash briefing: %s" _LOGGER.error(err, briefing_id) - return b"", HTTP_UNAUTHORIZED + return b"", HTTPStatus.UNAUTHORIZED if not hmac.compare_digest( request.query[API_PASSWORD].encode("utf-8"), @@ -66,12 +67,12 @@ class AlexaFlashBriefingView(http.HomeAssistantView): ): err = "Wrong password for Alexa flash briefing: %s" _LOGGER.error(err, briefing_id) - return b"", HTTP_UNAUTHORIZED + return b"", HTTPStatus.UNAUTHORIZED if not isinstance(self.flash_briefings.get(briefing_id), list): err = "No configured Alexa flash briefing was found for: %s" _LOGGER.error(err, briefing_id) - return b"", HTTP_NOT_FOUND + return b"", HTTPStatus.NOT_FOUND briefing = [] @@ -93,8 +94,7 @@ class AlexaFlashBriefingView(http.HomeAssistantView): else: output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT) - uid = item.get(CONF_UID) - if uid is None: + if (uid := item.get(CONF_UID)) is None: uid = str(uuid.uuid4()) output[ATTR_UID] = uid diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 5a23f5d1bc2..edf900bb18f 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -117,8 +117,7 @@ async def async_api_accept_grant(hass, config, directive, context): async def async_api_turn_on(hass, config, directive, context): """Process a turn on request.""" entity = directive.entity - domain = entity.domain - if domain == group.DOMAIN: + if (domain := entity.domain) == group.DOMAIN: domain = ha.DOMAIN service = SERVICE_TURN_ON @@ -1151,8 +1150,7 @@ async def async_api_adjust_range(hass, config, directive, context): if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": range_delta = int(range_delta * 20) if range_delta_default else int(range_delta) service = SERVICE_SET_COVER_POSITION - current = entity.attributes.get(cover.ATTR_POSITION) - if not current: + if not (current := entity.attributes.get(cover.ATTR_POSITION)): msg = f"Unable to determine {entity.entity_id} current position" raise AlexaInvalidValueError(msg) position = response_value = min(100, max(0, range_delta + current)) @@ -1188,8 +1186,7 @@ async def async_api_adjust_range(hass, config, directive, context): else int(range_delta) ) service = fan.SERVICE_SET_PERCENTAGE - current = entity.attributes.get(fan.ATTR_PERCENTAGE) - if not current: + if not (current := entity.attributes.get(fan.ATTR_PERCENTAGE)): msg = f"Unable to determine {entity.entity_id} current fan speed" raise AlexaInvalidValueError(msg) percentage = response_value = min(100, max(0, range_delta + current)) diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index f64031250e2..fede7d96810 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -120,9 +120,7 @@ async def async_handle_message(hass, message): req = message.get("request") req_type = req["type"] - handler = HANDLERS.get(req_type) - - if not handler: + if not (handler := HANDLERS.get(req_type)): raise UnknownRequest(f"Received unknown request {req_type}") return await handler(hass, message) diff --git a/homeassistant/components/alexa/logbook.py b/homeassistant/components/alexa/logbook.py index 153c7b7d61a..65fb410c601 100644 --- a/homeassistant/components/alexa/logbook.py +++ b/homeassistant/components/alexa/logbook.py @@ -12,9 +12,8 @@ def async_describe_events(hass, async_describe_event): def async_describe_logbook_event(event): """Describe a logbook event.""" data = event.data - entity_id = data["request"].get("entity_id") - if entity_id: + if entity_id := data["request"].get("entity_id"): state = hass.states.get(entity_id) name = state.name if state else entity_id message = f"sent command {data['request']['namespace']}/{data['request']['name']} for {name}" diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 41738c824fb..df4f95f12f2 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -3,7 +3,13 @@ import logging from homeassistant import core from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, +) +from homeassistant.helpers import entity_registry as er from .auth import Auth from .config import AbstractConfig @@ -60,7 +66,18 @@ class AlexaConfig(AbstractConfig): def should_expose(self, entity_id): """If an entity should be exposed.""" - return self._config[CONF_FILTER](entity_id) + if not self._config[CONF_FILTER].empty_filter: + return self._config[CONF_FILTER](entity_id) + + entity_registry = er.async_get(self.hass) + if registry_entry := entity_registry.async_get(entity_id): + auxiliary_entity = registry_entry.entity_category in ( + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ) + else: + auxiliary_entity = False + return not auxiliary_entity @core.callback def async_invalidate_access_token(self): diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 7a23706b4ba..e611960b9d9 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -2,13 +2,14 @@ from __future__ import annotations import asyncio +from http import HTTPStatus import json import logging import aiohttp import async_timeout -from homeassistant.const import HTTP_ACCEPTED, MATCH_ALL, STATE_ON +from homeassistant.const import MATCH_ALL, STATE_ON from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.significant_change import create_checker import homeassistant.util.dt as dt_util @@ -148,7 +149,7 @@ async def async_send_changereport_message( _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) _LOGGER.debug("Received (%s): %s", response.status, response_text) - if response.status == HTTP_ACCEPTED: + if response.status == HTTPStatus.ACCEPTED: return response_json = json.loads(response_text) @@ -279,7 +280,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity): _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) _LOGGER.debug("Received (%s): %s", response.status, response_text) - if response.status == HTTP_ACCEPTED: + if response.status == HTTPStatus.ACCEPTED: return response_json = json.loads(response_text) diff --git a/homeassistant/components/almond/translations/bg.json b/homeassistant/components/almond/translations/bg.json index bb0c874517b..81e1094b1ab 100644 --- a/homeassistant/components/almond/translations/bg.json +++ b/homeassistant/components/almond/translations/bg.json @@ -2,7 +2,8 @@ "config": { "abort": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430.", - "missing_configuration": "\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Almond." + "missing_configuration": "\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Almond.", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index 3fd57c17c63..42b19a52995 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -21,7 +21,6 @@ DOMAIN: Final = "ambee" LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(hours=1) -ATTR_ENTRY_TYPE: Final = "entry_type" ENTRY_TYPE_SERVICE: Final = "service" DEVICE_CLASS_AMBEE_RISK: Final = "ambee__risk" diff --git a/homeassistant/components/ambee/manifest.json b/homeassistant/components/ambee/manifest.json index e546f5009e8..3226e9de3a3 100644 --- a/homeassistant/components/ambee/manifest.json +++ b/homeassistant/components/ambee/manifest.json @@ -3,7 +3,7 @@ "name": "Ambee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambee", - "requirements": ["ambee==0.3.0"], + "requirements": ["ambee==0.4.0"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py index bd125ac973e..2ddd60a9168 100644 --- a/homeassistant/components/ambee/sensor.py +++ b/homeassistant/components/ambee/sensor.py @@ -7,8 +7,8 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) 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 import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import ATTR_ENTRY_TYPE, DOMAIN, ENTRY_TYPE_SERVICE, SENSORS, SERVICES +from .const import DOMAIN, ENTRY_TYPE_SERVICE, SENSORS, SERVICES async def async_setup_entry( @@ -58,12 +58,12 @@ class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): self.entity_description = description self._attr_unique_id = f"{entry_id}_{service_key}_{description.key}" - self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, f"{entry_id}_{service_key}")}, - ATTR_NAME: service, - ATTR_MANUFACTURER: "Ambee", - ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE, - } + self._attr_device_info = DeviceInfo( + entry_type=ENTRY_TYPE_SERVICE, + identifiers={(DOMAIN, f"{entry_id}_{service_key}")}, + manufacturer="Ambee", + name=service, + ) @property def native_value(self) -> StateType: diff --git a/homeassistant/components/ambee/translations/bg.json b/homeassistant/components/ambee/translations/bg.json new file mode 100644 index 00000000000..c72dc5227ca --- /dev/null +++ b/homeassistant/components/ambee/translations/bg.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/hu.json b/homeassistant/components/ambee/translations/hu.json index 6cb59bba925..e4cef44c5ba 100644 --- a/homeassistant/components/ambee/translations/hu.json +++ b/homeassistant/components/ambee/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/ambee/translations/ru.json b/homeassistant/components/ambee/translations/ru.json index 5fb89879a4e..02458c7609f 100644 --- a/homeassistant/components/ambee/translations/ru.json +++ b/homeassistant/components/ambee/translations/ru.json @@ -11,7 +11,7 @@ "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." + "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": { diff --git a/homeassistant/components/amberelectric/binary_sensor.py b/homeassistant/components/amberelectric/binary_sensor.py index aff19c6f695..fe6edea18f8 100644 --- a/homeassistant/components/amberelectric/binary_sensor.py +++ b/homeassistant/components/amberelectric/binary_sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -28,6 +27,8 @@ PRICE_SPIKE_ICONS = { class AmberPriceGridSensor(CoordinatorEntity, BinarySensorEntity): """Sensor to show single grid binary values.""" + _attr_attribution = ATTRIBUTION + def __init__( self, coordinator: AmberUpdateCoordinator, @@ -38,7 +39,6 @@ class AmberPriceGridSensor(CoordinatorEntity, BinarySensorEntity): self.site_id = coordinator.site_id self.entity_description = description self._attr_unique_id = f"{coordinator.site_id}-{description.key}" - self._attr_device_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} @property def is_on(self) -> bool | None: @@ -67,7 +67,6 @@ class AmberPriceSpikeBinarySensor(AmberPriceGridSensor): spike_status = self.coordinator.data["grid"]["price_spike"] return { "spike_status": spike_status, - ATTR_ATTRIBUTION: ATTRIBUTION, } diff --git a/homeassistant/components/amberelectric/sensor.py b/homeassistant/components/amberelectric/sensor.py index 974de2d5c15..a1644fb7924 100644 --- a/homeassistant/components/amberelectric/sensor.py +++ b/homeassistant/components/amberelectric/sensor.py @@ -20,7 +20,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_DOLLAR, ENERGY_KILO_WATT_HOUR +from homeassistant.const import CURRENCY_DOLLAR, ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -54,6 +54,8 @@ def friendly_channel_type(channel_type: str) -> str: class AmberSensor(CoordinatorEntity, SensorEntity): """Amber Base Sensor.""" + _attr_attribution = ATTRIBUTION + def __init__( self, coordinator: AmberUpdateCoordinator, @@ -88,7 +90,7 @@ class AmberPriceSensor(AmberSensor): """Return additional pieces of information about the price.""" interval = self.coordinator.data[self.entity_description.key][self.channel_type] - data: dict[str, Any] = {ATTR_ATTRIBUTION: ATTRIBUTION} + data: dict[str, Any] = {} if interval is None: return data @@ -143,7 +145,6 @@ class AmberForecastSensor(AmberSensor): data = { "forecasts": [], "channel_type": intervals[0].channel_type.value, - ATTR_ATTRIBUTION: ATTRIBUTION, } for interval in intervals: @@ -172,6 +173,8 @@ class AmberForecastSensor(AmberSensor): class AmberGridSensor(CoordinatorEntity, SensorEntity): """Sensor to show single grid specific values.""" + _attr_attribution = ATTRIBUTION + def __init__( self, coordinator: AmberUpdateCoordinator, @@ -181,7 +184,6 @@ class AmberGridSensor(CoordinatorEntity, SensorEntity): super().__init__(coordinator) self.site_id = coordinator.site_id self.entity_description = description - self._attr_device_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attr_unique_id = f"{coordinator.site_id}-{description.key}" @property diff --git a/homeassistant/components/amberelectric/translations/bg.json b/homeassistant/components/amberelectric/translations/bg.json new file mode 100644 index 00000000000..5cfcc05b133 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "site": { + "title": "Amber Electric" + }, + "user": { + "description": "\u041e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 {api_url}, \u0437\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 API \u043a\u043b\u044e\u0447", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/ja.json b/homeassistant/components/amberelectric/translations/ja.json new file mode 100644 index 00000000000..e0a3590c8b4 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_token": "API\u30c8\u30fc\u30af\u30f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/pl.json b/homeassistant/components/amberelectric/translations/pl.json new file mode 100644 index 00000000000..1054014ea49 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/pl.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index aa4be202865..67fd3adeec7 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -21,6 +21,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from .const import ( ATTR_VALUE, @@ -149,16 +150,15 @@ class AmbiclimateEntity(ClimateEntity): self._store = store self._attr_unique_id = heater.device_id self._attr_name = heater.name - self._attr_device_info = { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Ambiclimate", - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Ambiclimate", + name=self.name, + ) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return await self._heater.set_target_temperature(temperature) diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 04d1b749d10..623e96a4a67 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -143,8 +143,7 @@ class AmbiclimateAuthCallbackView(HomeAssistantView): async def get(self, request: web.Request) -> str: """Receive authorization token.""" # pylint: disable=no-self-use - code = request.query.get("code") - if code is None: + if (code := request.query.get("code")) is None: return "No code" hass = request.app["hass"] hass.async_create_task( diff --git a/homeassistant/components/ambiclimate/translations/bg.json b/homeassistant/components/ambiclimate/translations/bg.json index 627dd472018..35a413e3627 100644 --- a/homeassistant/components/ambiclimate/translations/bg.json +++ b/homeassistant/components/ambiclimate/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "access_token": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f." + "access_token": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f.", + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" }, "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Ambiclimate." diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 1f1b21b4346..190ed6dc59e 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from aioambient import Client +from aioambient import Websocket from aioambient.errors import WebsocketError from homeassistant.config_entries import ConfigEntry @@ -15,18 +15,17 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.event import async_call_later from .const import ( ATTR_LAST_DATA, CONF_APP_KEY, - DATA_CLIENT, DOMAIN, LOGGER, TYPE_SOLARRADIATION, @@ -57,37 +56,32 @@ def async_hydrate_station_data(data: dict[str, Any]) -> dict[str, Any]: return data -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Ambient PWS as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} - if not config_entry.unique_id: + if not entry.unique_id: hass.config_entries.async_update_entry( - config_entry, unique_id=config_entry.data[CONF_APP_KEY] + entry, unique_id=entry.data[CONF_APP_KEY] ) - session = aiohttp_client.async_get_clientsession(hass) try: ambient = AmbientStation( hass, - config_entry, - Client( - config_entry.data[CONF_API_KEY], - config_entry.data[CONF_APP_KEY], - session=session, - logger=LOGGER, - ), + entry, + Websocket(entry.data[CONF_APP_KEY], entry.data[CONF_API_KEY]), ) hass.loop.create_task(ambient.ws_connect()) - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient + hass.data[DOMAIN][entry.entry_id] = ambient except WebsocketError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err async def _async_disconnect_websocket(_: Event) -> None: - await ambient.client.websocket.disconnect() + await ambient.websocket.disconnect() - config_entry.async_on_unload( + entry.async_on_unload( hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket ) @@ -96,30 +90,32 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an Ambient PWS config entry.""" - ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) - hass.async_create_task(ambient.ws_disconnect()) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + ambient = hass.data[DOMAIN].pop(entry.entry_id) + hass.async_create_task(ambient.ws_disconnect()) - return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + return unload_ok -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate old entry.""" - version = config_entry.version + version = entry.version LOGGER.debug("Migrating from version %s", version) # 1 -> 2: Unique ID format changed, so delete and re-import: if version == 1: dev_reg = await hass.helpers.device_registry.async_get_registry() - dev_reg.async_clear_config_entry(config_entry) + dev_reg.async_clear_config_entry(entry) en_reg = await hass.helpers.entity_registry.async_get_registry() - en_reg.async_clear_config_entry(config_entry) + en_reg.async_clear_config_entry(entry) - version = config_entry.version = 2 - hass.config_entries.async_update_entry(config_entry) + version = entry.version = 2 + hass.config_entries.async_update_entry(entry) LOGGER.info("Migration to version %s successful", version) return True @@ -129,22 +125,22 @@ class AmbientStation: """Define a class to handle the Ambient websocket.""" def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, client: Client + self, hass: HomeAssistant, entry: ConfigEntry, websocket: Websocket ) -> None: """Initialize.""" - self._config_entry = config_entry + self._entry = entry self._entry_setup_complete = False self._hass = hass self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY - self.client = client self.stations: dict[str, dict] = {} + self.websocket = websocket async def _attempt_connect(self) -> None: """Attempt to connect to the socket (retrying later on fail).""" async def connect(timestamp: int | None = None) -> None: """Connect.""" - await self.client.websocket.connect() + await self.websocket.connect() try: await connect() @@ -194,22 +190,20 @@ class AmbientStation: # attempt forward setup of the config entry (because it will have # already been done): if not self._entry_setup_complete: - self._hass.config_entries.async_setup_platforms( - self._config_entry, PLATFORMS - ) + self._hass.config_entries.async_setup_platforms(self._entry, PLATFORMS) self._entry_setup_complete = True self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY - self.client.websocket.on_connect(on_connect) - self.client.websocket.on_data(on_data) - self.client.websocket.on_disconnect(on_disconnect) - self.client.websocket.on_subscribed(on_subscribed) + self.websocket.on_connect(on_connect) + self.websocket.on_data(on_data) + self.websocket.on_disconnect(on_disconnect) + self.websocket.on_subscribed(on_subscribed) await self._attempt_connect() async def ws_disconnect(self) -> None: """Disconnect from the websocket.""" - await self.client.websocket.disconnect() + await self.websocket.disconnect() class AmbientWeatherEntity(Entity): @@ -226,11 +220,11 @@ class AmbientWeatherEntity(Entity): ) -> None: """Initialize the entity.""" self._ambient = ambient - self._attr_device_info = { - "identifiers": {(DOMAIN, mac_address)}, - "name": station_name, - "manufacturer": "Ambient Weather", - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, mac_address)}, + manufacturer="Ambient Weather", + name=station_name, + ) self._attr_name = f"{station_name}_{description.name}" self._attr_unique_id = f"{mac_address}_{description.key}" self._mac_address = mac_address diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index e513486fb85..0c819a9a9b7 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -11,12 +11,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_NAME +from homeassistant.const import ATTR_NAME, ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AmbientWeatherEntity -from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN +from .const import ATTR_LAST_DATA, DOMAIN TYPE_BATT1 = "batt1" TYPE_BATT10 = "batt10" @@ -63,144 +63,168 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=TYPE_BATTOUT, name="Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT1, name="Battery 1", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT2, name="Battery 2", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT3, name="Battery 3", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT4, name="Battery 4", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT5, name="Battery 5", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT6, name="Battery 6", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT7, name="Battery 7", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT8, name="Battery 8", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT9, name="Battery 9", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT10, name="Battery 10", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_CO2, name="CO2 Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25IN_BATT, name="PM25 Indoor Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25_BATT, name="PM25 Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_RELAY1, name="Relay 1", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY2, name="Relay 2", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY3, name="Relay 3", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY4, name="Relay 4", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY5, name="Relay 5", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY6, name="Relay 6", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY7, name="Relay 7", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY8, name="Relay 8", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY9, name="Relay 9", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY10, name="Relay 10", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), ) @@ -210,7 +234,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ambient PWS binary sensors based on a config entry.""" - ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + ambient = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py index d93d502ac92..2c2d231b33e 100644 --- a/homeassistant/components/ambient_station/config_flow.py +++ b/homeassistant/components/ambient_station/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure the Ambient PWS component.""" from __future__ import annotations -from aioambient import Client +from aioambient import API from aioambient.errors import AmbientError import voluptuous as vol @@ -41,12 +41,10 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() session = aiohttp_client.async_get_clientsession(self.hass) - client = Client( - user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session=session - ) + api = API(user_input[CONF_APP_KEY], user_input[CONF_API_KEY], session=session) try: - devices = await client.api.get_devices() + devices = await api.get_devices() except AmbientError: return await self._show_form({"base": "invalid_key"}) diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py index cf5c97be045..4e0ec598fb1 100644 --- a/homeassistant/components/ambient_station/const.py +++ b/homeassistant/components/ambient_station/const.py @@ -8,7 +8,5 @@ ATTR_LAST_DATA = "last_data" CONF_APP_KEY = "app_key" -DATA_CLIENT = "data_client" - TYPE_SOLARRADIATION = "solarradiation" TYPE_SOLARRADIATION_LX = "solarradiation_lx" diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index b95f4a8f13c..857ce6de585 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -3,7 +3,7 @@ "name": "Ambient Weather Station", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", - "requirements": ["aioambient==1.3.0"], + "requirements": ["aioambient==2021.10.1"], "codeowners": ["@bachya"], "iot_class": "cloud_push" } diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 0247d03b6fd..66bccb30b55 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -39,7 +39,7 @@ from . import ( AmbientStation, AmbientWeatherEntity, ) -from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN +from .const import ATTR_LAST_DATA, DOMAIN TYPE_24HOURRAININ = "24hourrainin" TYPE_BAROMABSIN = "baromabsin" @@ -609,7 +609,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ambient PWS sensors based on a config entry.""" - ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + ambient = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ diff --git a/homeassistant/components/ambient_station/translations/hu.json b/homeassistant/components/ambient_station/translations/hu.json index 7c7e3a658b9..6974bda2c20 100644 --- a/homeassistant/components/ambient_station/translations/hu.json +++ b/homeassistant/components/ambient_station/translations/hu.json @@ -13,7 +13,7 @@ "api_key": "API kulcs", "app_key": "Alkalmaz\u00e1skulcs" }, - "title": "T\u00f6ltsd ki az adataid" + "title": "T\u00f6ltse ki az adatait" } } } diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index 8d2535a142b..4fc810fd773 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -215,8 +215,7 @@ class AmcrestBinarySensor(BinarySensorEntity): log_update_error(_LOGGER, "update", self.name, "binary sensor", error) return - event_code = self.entity_description.event_code - if event_code is None: + if (event_code := self.entity_description.event_code) is None: _LOGGER.error("Binary sensor %s event code not set", self.name) return @@ -228,12 +227,10 @@ class AmcrestBinarySensor(BinarySensorEntity): def _update_unique_id(self) -> None: """Set the unique id.""" - if self._attr_unique_id is None: - serial_number = self._api.serial_number - if serial_number: - self._attr_unique_id = ( - f"{serial_number}-{self.entity_description.key}-{self._channel}" - ) + if self._attr_unique_id is None and (serial_number := self._api.serial_number): + self._attr_unique_id = ( + f"{serial_number}-{self.entity_description.key}-{self._channel}" + ) async def async_on_demand_update(self) -> None: """Update state.""" diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index f2048654da6..c262e16ec7c 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -96,18 +96,14 @@ class AmcrestSensor(SensorEntity): _LOGGER.debug("Updating %s sensor", self.name) sensor_type = self.entity_description.key - if self._attr_unique_id is None: - serial_number = self._api.serial_number - if serial_number: - self._attr_unique_id = f"{serial_number}-{sensor_type}-{self._channel}" + if self._attr_unique_id is None and (serial_number := self._api.serial_number): + self._attr_unique_id = f"{serial_number}-{sensor_type}-{self._channel}" try: - if self._attr_unique_id is None: - serial_number = self._api.serial_number - if serial_number: - self._attr_unique_id = ( - f"{serial_number}-{sensor_type}-{self._channel}" - ) + if self._attr_unique_id is None and ( + serial_number := self._api.serial_number + ): + self._attr_unique_id = f"{serial_number}-{sensor_type}-{self._channel}" if sensor_type == SENSOR_PTZ_PRESET: self._attr_native_value = self._api.ptz_presets_count diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 533470181c1..89deeec25b8 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -533,8 +533,7 @@ class ADBDevice(MediaPlayerEntity): @adb_decorator() async def adb_command(self, cmd): """Send an ADB command to an Android TV / Fire TV device.""" - key = KEYS.get(cmd) - if key: + if key := KEYS.get(cmd): await self.aftv.adb_shell(f"input keyevent {key}") return diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 01d48a190fd..229311ff6d9 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -97,8 +97,7 @@ class APIEventStream(HomeAssistantView): stop_obj = object() to_write = asyncio.Queue() - restrict = request.query.get("restrict") - if restrict: + if restrict := request.query.get("restrict"): restrict = restrict.split(",") + [EVENT_HOMEASSISTANT_STOP] async def forward_events(event): @@ -225,8 +224,7 @@ class APIEntityStateView(HomeAssistantView): if not user.permissions.check_entity(entity_id, POLICY_READ): raise Unauthorized(entity_id=entity_id) - state = request.app["hass"].states.get(entity_id) - if state: + if state := request.app["hass"].states.get(entity_id): return self.json(state) return self.json_message("Entity not found.", HTTPStatus.NOT_FOUND) @@ -240,9 +238,7 @@ class APIEntityStateView(HomeAssistantView): except ValueError: return self.json_message("Invalid JSON specified.", HTTPStatus.BAD_REQUEST) - new_state = data.get("state") - - if new_state is None: + if (new_state := data.get("state")) is None: return self.json_message("No state specified.", HTTPStatus.BAD_REQUEST) attributes = data.get("attributes") diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index a87cae09b1a..e0287897e96 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -220,9 +220,7 @@ class ApnsNotificationService(BaseNotificationService): ) device_state = kwargs.get(ATTR_TARGET) - message_data = kwargs.get(ATTR_DATA) - - if message_data is None: + if (message_data := kwargs.get(ATTR_DATA)) is None: message_data = {} if isinstance(message, str): diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index c4efa4ca09a..fbf02fcfdff 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -10,6 +10,13 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( + ATTR_CONNECTIONS, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SUGGESTED_AREA, + ATTR_SW_VERSION, CONF_ADDRESS, CONF_NAME, CONF_PROTOCOL, @@ -22,7 +29,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN @@ -91,9 +98,7 @@ class AppleTVEntity(Entity): self.manager = manager self._attr_name = name self._attr_unique_id = identifier - self._attr_device_info = { - "identifiers": {(DOMAIN, identifier)}, - } + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, identifier)}) async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" @@ -324,25 +329,27 @@ class AppleTVManager: async def _async_setup_device_registry(self): attrs = { - "identifiers": {(DOMAIN, self.config_entry.unique_id)}, - "manufacturer": "Apple", - "name": self.config_entry.data[CONF_NAME], + ATTR_IDENTIFIERS: {(DOMAIN, self.config_entry.unique_id)}, + ATTR_MANUFACTURER: "Apple", + ATTR_NAME: self.config_entry.data[CONF_NAME], } - area = attrs["name"] + area = attrs[ATTR_NAME] name_trailer = f" {DEFAULT_NAME}" if area.endswith(name_trailer): area = area[: -len(name_trailer)] - attrs["suggested_area"] = area + attrs[ATTR_SUGGESTED_AREA] = area if self.atv: dev_info = self.atv.device_info - attrs["model"] = DEFAULT_NAME + " " + dev_info.model.name.replace("Gen", "") - attrs["sw_version"] = dev_info.version + attrs[ATTR_MODEL] = ( + DEFAULT_NAME + " " + dev_info.model.name.replace("Gen", "") + ) + attrs[ATTR_SW_VERSION] = dev_info.version if dev_info.mac: - attrs["connections"] = {(dr.CONNECTION_NETWORK_MAC, dev_info.mac)} + attrs[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, dev_info.mac)} device_registry = await dr.async_get_registry(self.hass) device_registry.async_get_or_create( diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 09ecc01015c..35b9777394e 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -6,6 +6,7 @@ from pyatv.const import ( FeatureName, FeatureState, MediaType, + PowerState, RepeatState, ShuffleState, ) @@ -87,12 +88,14 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): """Handle when connection is made to device.""" self.atv.push_updater.listener = self self.atv.push_updater.start() + self.atv.power.listener = self @callback def async_device_disconnected(self): """Handle when connection was lost to device.""" self.atv.push_updater.stop() self.atv.push_updater.listener = None + self.atv.power.listener = None @property def state(self): @@ -101,6 +104,11 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return None if self.atv is None: return STATE_OFF + if ( + self._is_feature_available(FeatureName.PowerState) + and self.atv.power.power_state == PowerState.Off + ): + return STATE_STANDBY if self._playing: state = self._playing.device_state if state in (DeviceState.Idle, DeviceState.Loading): @@ -125,6 +133,11 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): self._playing = None self.async_write_ha_state() + @callback + def powerstate_update(self, old_state: PowerState, new_state: PowerState): + """Update power state when it changes.""" + self.async_write_ha_state() + @property def app_id(self): """ID of the current running app.""" @@ -239,12 +252,16 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): async def async_turn_on(self): """Turn the media player on.""" - await self.manager.connect() + if self._is_feature_available(FeatureName.TurnOn): + await self.atv.power.turn_on() async def async_turn_off(self): """Turn the media player off.""" - self._playing = None - await self.manager.disconnect() + if (self._is_feature_available(FeatureName.TurnOff)) and ( + not self._is_feature_available(FeatureName.PowerState) + or self.atv.power.power_state == PowerState.On + ): + await self.atv.power.turn_off() async def async_media_play_pause(self): """Pause media on media player.""" diff --git a/homeassistant/components/apple_tv/translations/bg.json b/homeassistant/components/apple_tv/translations/bg.json new file mode 100644 index 00000000000..5a7ae474102 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/bg.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "pair_with_pin": { + "data": { + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + } + }, + "reconfigure": { + "title": "\u041f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" + }, + "service_problem": { + "title": "\u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435\u0442\u043e \u043d\u0430 \u0443\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "user": { + "data": { + "device_input": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + } + } + }, + "title": "Apple TV" +} \ 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 3d254422baf..3ee74bbf419 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "backoff": "Az eszk\u00f6z jelenleg nem fogadja el a p\u00e1ros\u00edt\u00e1si k\u00e9relmeket (lehet, hogy t\u00fal sokszor adott meg \u00e9rv\u00e9nytelen PIN-k\u00f3dot), pr\u00f3b\u00e1lkozzon \u00fajra k\u00e9s\u0151bb.", "device_did_not_pair": "A p\u00e1ros\u00edt\u00e1s folyamat\u00e1t az eszk\u00f6zr\u0151l nem pr\u00f3b\u00e1lt\u00e1k befejezni.", "invalid_config": "Az eszk\u00f6z konfigur\u00e1l\u00e1sa nem teljes. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", @@ -23,14 +23,14 @@ "title": "Apple TV sikeresen hozz\u00e1adva" }, "pair_no_pin": { - "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a(z) {protocol} szolg\u00e1ltat\u00e1shoz. A folytat\u00e1shoz k\u00e9rj\u00fck, \u00edrja be az Apple TV {pin} -t.", + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} szolg\u00e1ltat\u00e1shoz. A folytat\u00e1shoz k\u00e9rj\u00fck, \u00edrja be az Apple TV {pin}-t.", "title": "P\u00e1ros\u00edt\u00e1s" }, "pair_with_pin": { "data": { "pin": "PIN-k\u00f3d" }, - "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a(z) {protocol} protokollhoz. K\u00e9rj\u00fck, adja meg a k\u00e9perny\u0151n megjelen\u0151 PIN-k\u00f3dot. A vezet\u0151 null\u00e1kat el kell hagyni, pl. \u00edrja be a 123 \u00e9rt\u00e9ket, ha a megjelen\u00edtett k\u00f3d 0123.", + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} protokollhoz. K\u00e9rj\u00fck, adja meg a k\u00e9perny\u0151n megjelen\u0151 PIN-k\u00f3dot. A vezet\u0151 null\u00e1kat el kell hagyni, pl. \u00edrja be a 123 \u00e9rt\u00e9ket, ha a megjelen\u00edtett k\u00f3d 0123.", "title": "P\u00e1ros\u00edt\u00e1s" }, "reconfigure": { @@ -38,7 +38,7 @@ "title": "Eszk\u00f6z \u00fajrakonfigur\u00e1l\u00e1sa" }, "service_problem": { - "description": "Hiba t\u00f6rt\u00e9nt a(z) \" {protocol} \" protokoll p\u00e1ros\u00edt\u00e1sakor. Ez figyelmen k\u00edv\u00fcl lett hagyva.", + "description": "Hiba t\u00f6rt\u00e9nt a `{protocol}` protokoll p\u00e1ros\u00edt\u00e1sakor. Figyelmen k\u00edv\u00fcl lesz hagyva.", "title": "Nem siker\u00fclt hozz\u00e1adni a szolg\u00e1ltat\u00e1st" }, "user": { diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 4c46a7aa5eb..c1823ca2bb5 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -147,8 +147,7 @@ class AquaLogicSensor(SensorEntity): @callback def async_update_callback(self): """Update callback.""" - panel = self._processor.panel - if panel is not None: + if (panel := self._processor.panel) is not None: if panel.is_metric: self._attr_native_unit_of_measurement = ( self.entity_description.unit_metric diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index c05bacc5f03..157688c7576 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -66,23 +66,20 @@ class AquaLogicSwitch(SwitchEntity): @property def is_on(self): """Return true if device is on.""" - panel = self._processor.panel - if panel is None: + if (panel := self._processor.panel) is None: return False state = panel.get_state(self._state_name) return state def turn_on(self, **kwargs): """Turn the device on.""" - panel = self._processor.panel - if panel is None: + if (panel := self._processor.panel) is None: return panel.set_state(self._state_name, True) def turn_off(self, **kwargs): """Turn the device off.""" - panel = self._processor.panel - if panel is None: + if (panel := self._processor.panel) is None: return panel.set_state(self._state_name, False) diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 6685ea240eb..08545f4c5b0 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -3,7 +3,7 @@ "name": "Arcam FMJ Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", - "requirements": ["arcam-fmj==0.7.0"], + "requirements": ["arcam-fmj==0.12.0"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index e9e5e29a1c7..b63279d9c26 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -1,7 +1,7 @@ """Arcam media player.""" import logging -from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes +from arcam.fmj import SourceCodes from arcam.fmj.state import State from homeassistant import config_entries @@ -23,6 +23,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.errors import BrowseError from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from .config_flow import get_entry_client from .const import ( @@ -91,19 +92,6 @@ class ArcamFmj(MediaPlayerEntity): self._attr_unique_id = f"{uuid}-{state.zn}" self._attr_entity_registry_enabled_default = state.zn == 1 - def _get_2ch(self): - """Return if source is 2 channel or not.""" - audio_format, _ = self._state.get_incoming_audio_format() - return bool( - audio_format - in ( - IncomingAudioFormat.PCM, - IncomingAudioFormat.ANALOGUE_DIRECT, - IncomingAudioFormat.UNDETECTED, - None, - ) - ) - @property def state(self): """Return the state of the device.""" @@ -114,19 +102,20 @@ class ArcamFmj(MediaPlayerEntity): @property def device_info(self): """Return a device description for device registry.""" - return { - "name": self._device_name, - "identifiers": { + return DeviceInfo( + identifiers={ (DOMAIN, self._uuid), (DOMAIN, self._state.client.host, self._state.client.port), }, - "model": "Arcam FMJ AVR", - "manufacturer": "Arcam", - } + manufacturer="Arcam", + model="Arcam FMJ AVR", + name=self._device_name, + ) async def async_added_to_hass(self): """Once registered, add listener for events.""" await self._state.start() + await self._state.update() @callback def _data(host): @@ -185,11 +174,8 @@ class ArcamFmj(MediaPlayerEntity): async def async_select_sound_mode(self, sound_mode): """Select a specific source.""" try: - if self._get_2ch(): - await self._state.set_decode_mode_2ch(DecodeMode2CH[sound_mode]) - else: - await self._state.set_decode_mode_mch(DecodeModeMCH[sound_mode]) - except KeyError: + await self._state.set_decode_mode(sound_mode) + except (KeyError, ValueError): _LOGGER.error("Unsupported sound_mode %s", sound_mode) return @@ -282,26 +268,18 @@ class ArcamFmj(MediaPlayerEntity): @property def sound_mode(self): """Name of the current sound mode.""" - if self._state.zn != 1: + value = self._state.get_decode_mode() + if value is None: return None - - if self._get_2ch(): - value = self._state.get_decode_mode_2ch() - else: - value = self._state.get_decode_mode_mch() - if value: - return value.name - return None + return value.name @property def sound_mode_list(self): """List of available sound modes.""" - if self._state.zn != 1: + values = self._state.get_decode_modes() + if values is None: return None - - if self._get_2ch(): - return [x.name for x in DecodeMode2CH] - return [x.name for x in DecodeModeMCH] + return [x.name for x in values] @property def is_volume_muted(self): @@ -375,9 +353,7 @@ class ArcamFmj(MediaPlayerEntity): if source is None: return None - channel = self.media_channel - - if channel: + if channel := self.media_channel: value = f"{source.name} - {channel}" else: value = source.name diff --git a/homeassistant/components/arcam_fmj/translations/bg.json b/homeassistant/components/arcam_fmj/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ 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 c7532f24b76..964ebe2a33d 100644 --- a/homeassistant/components/arcam_fmj/translations/hu.json +++ b/homeassistant/components/arcam_fmj/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index d59e6d0cccb..1280e013f8d 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -1,5 +1,6 @@ """Support for an exposed aREST RESTful API of a device.""" from datetime import timedelta +from http import HTTPStatus import logging import requests @@ -10,13 +11,7 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, ) -from homeassistant.const import ( - CONF_DEVICE_CLASS, - CONF_NAME, - CONF_PIN, - CONF_RESOURCE, - HTTP_OK, -) +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_PIN, CONF_RESOURCE import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -78,7 +73,7 @@ class ArestBinarySensor(BinarySensorEntity): if pin is not None: request = requests.get(f"{resource}/mode/{pin}/i", timeout=10) - if request.status_code != HTTP_OK: + if request.status_code != HTTPStatus.OK: _LOGGER.error("Can't set mode of %s", resource) def update(self): diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index addd666e30e..7ca6d230a08 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -1,5 +1,6 @@ """Support for an exposed aREST RESTful API of a device.""" from datetime import timedelta +from http import HTTPStatus import logging import requests @@ -12,7 +13,6 @@ from homeassistant.const import ( CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, - HTTP_OK, ) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -146,7 +146,7 @@ class ArestSensor(SensorEntity): if pin is not None: request = requests.get(f"{resource}/mode/{pin}/i", timeout=10) - if request.status_code != HTTP_OK: + if request.status_code != HTTPStatus.OK: _LOGGER.error("Can't set mode of %s", resource) def update(self): diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index ecbf24c23ca..97a763cb652 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -1,12 +1,13 @@ """Support for an exposed aREST RESTful API of a device.""" +from http import HTTPStatus import logging import requests import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_NAME, CONF_RESOURCE, HTTP_OK +from homeassistant.const import CONF_NAME, CONF_RESOURCE import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -101,7 +102,7 @@ class ArestSwitchFunction(ArestSwitchBase): request = requests.get(f"{self._resource}/{self._func}", timeout=10) - if request.status_code != HTTP_OK: + if request.status_code != HTTPStatus.OK: _LOGGER.error("Can't find function") return @@ -118,7 +119,7 @@ class ArestSwitchFunction(ArestSwitchBase): f"{self._resource}/{self._func}", timeout=10, params={"params": "1"} ) - if request.status_code == HTTP_OK: + if request.status_code == HTTPStatus.OK: self._attr_is_on = True else: _LOGGER.error("Can't turn on function %s at %s", self._func, self._resource) @@ -129,7 +130,7 @@ class ArestSwitchFunction(ArestSwitchBase): f"{self._resource}/{self._func}", timeout=10, params={"params": "0"} ) - if request.status_code == HTTP_OK: + if request.status_code == HTTPStatus.OK: self._attr_is_on = False else: _LOGGER.error( @@ -157,7 +158,7 @@ class ArestSwitchPin(ArestSwitchBase): self.invert = invert request = requests.get(f"{resource}/mode/{pin}/o", timeout=10) - if request.status_code != HTTP_OK: + if request.status_code != HTTPStatus.OK: _LOGGER.error("Can't set mode") self._attr_available = False @@ -167,7 +168,7 @@ class ArestSwitchPin(ArestSwitchBase): request = requests.get( f"{self._resource}/digital/{self._pin}/{turn_on_payload}", timeout=10 ) - if request.status_code == HTTP_OK: + if request.status_code == HTTPStatus.OK: self._attr_is_on = True else: _LOGGER.error("Can't turn on pin %s at %s", self._pin, self._resource) @@ -178,7 +179,7 @@ class ArestSwitchPin(ArestSwitchBase): request = requests.get( f"{self._resource}/digital/{self._pin}/{turn_off_payload}", timeout=10 ) - if request.status_code == HTTP_OK: + if request.status_code == HTTPStatus.OK: self._attr_is_on = False else: _LOGGER.error("Can't turn off pin %s at %s", self._pin, self._resource) diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index 6b14f0cee0c..146970a8610 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -145,13 +145,12 @@ class ArloCam(Camera): def set_base_station_mode(self, mode): """Set the mode in the base station.""" # Get the list of base stations identified by library - base_stations = self.hass.data[DATA_ARLO].base_stations # Some Arlo cameras does not have base station # So check if there is base station detected first # if yes, then choose the primary base station # Set the mode on the chosen base station - if base_stations: + if base_stations := self.hass.data[DATA_ARLO].base_stations: primary_base_station = base_stations[0] primary_base_station.mode = mode diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index 57c897cfd59..7fbc57f9c6b 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import ( - ATTR_ATTRIBUTION, CONCENTRATION_PARTS_PER_MILLION, CONF_MONITORED_CONDITIONS, DEVICE_CLASS_BATTERY, @@ -90,8 +89,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up an Arlo IP sensor.""" - arlo = hass.data.get(DATA_ARLO) - if not arlo: + if not (arlo := hass.data.get(DATA_ARLO)): return sensors = [] @@ -123,6 +121,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ArloSensor(SensorEntity): """An implementation of a Netgear Arlo IP sensor.""" + _attr_attribution = ATTRIBUTION + def __init__(self, device, sensor_entry): """Initialize an Arlo sensor.""" self.entity_description = sensor_entry @@ -212,7 +212,6 @@ class ArloSensor(SensorEntity): """Return the device state attributes.""" attrs = {} - attrs[ATTR_ATTRIBUTION] = ATTRIBUTION attrs["brand"] = DEFAULT_BRAND if self.entity_description.key != "total_cameras": diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 321be5035cd..2571d35f98e 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -95,8 +95,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if not sensors: return - store = hass.data.get(DATA_ARWN) - if store is None: + if (store := hass.data.get(DATA_ARWN)) is None: store = hass.data[DATA_ARWN] = {} if isinstance(sensors, ArwnSensor): diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index c87ed85b759..2d067d0e608 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -73,8 +73,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the AsusWrt integration.""" - conf = config.get(DOMAIN) - if conf is None: + if (conf := config.get(DOMAIN)) is None: return True # save the options from config yaml @@ -142,7 +141,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index c48ea4d57fe..5a20880b4b0 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -217,8 +217,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): } ) - conf_mode = self.config_entry.data[CONF_MODE] - if conf_mode == MODE_AP: + if self.config_entry.data[CONF_MODE] == MODE_AP: data_schema = data_schema.extend( { vol.Optional( diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index d5d3d9026b5..380f7a60c32 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -4,9 +4,11 @@ from __future__ import annotations from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_DEFAULT_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from .const import DATA_ASUSWRT, DOMAIN from .router import AsusWrtRouter @@ -60,12 +62,12 @@ class AsusWrtDevice(ScannerEntity): self._device = device self._attr_unique_id = device.mac self._attr_name = device.name or DEFAULT_DEVICE_NAME - self._attr_device_info = { - "connections": {(CONNECTION_NETWORK_MAC, device.mac)}, - "default_model": "ASUSWRT Tracked device", - } + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, device.mac)}, + default_model="ASUSWRT Tracked device", + ) if device.name: - self._attr_device_info["default_name"] = device.name + self._attr_device_info[ATTR_DEFAULT_NAME] = device.name @property def is_connected(self): diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 7e89ea07dbd..03a15f80110 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -386,13 +386,14 @@ class AsusWrtRouter: @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "identifiers": {(DOMAIN, "AsusWRT")}, - "name": self._host, - "model": self._model, - "manufacturer": "Asus", - "sw_version": self._sw_v, - } + return DeviceInfo( + identifiers={(DOMAIN, "AsusWRT")}, + name=self._host, + model=self._model, + manufacturer="Asus", + sw_version=self._sw_v, + configuration_url=f"http://{self._host}", + ) @property def signal_device_new(self) -> str: diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 5392b419bca..8beb5d3d9ee 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -import logging from numbers import Real from homeassistant.components.sensor import ( @@ -12,7 +11,11 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND +from homeassistant.const import ( + DATA_GIGABYTES, + DATA_RATE_MEGABITS_PER_SECOND, + ENTITY_CATEGORY_DIAGNOSTIC, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -90,6 +93,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Load Avg (1m)", icon="mdi:cpu-32-bit", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, precision=1, @@ -99,6 +103,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Load Avg (5m)", icon="mdi:cpu-32-bit", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, precision=1, @@ -108,14 +113,13 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Load Avg (15m)", icon="mdi:cpu-32-bit", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, precision=1, ), ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index de785a3a317..69880da5a39 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -81,10 +81,10 @@ class AtagEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return info for device registry.""" - return { - "identifiers": {(DOMAIN, self.coordinator.data.id)}, - "name": "Atag Thermostat", - "model": "Atag One", - "sw_version": self.coordinator.data.apiversion, - "manufacturer": "Atag", - } + return DeviceInfo( + identifiers={(DOMAIN, self.coordinator.data.id)}, + manufacturer="Atag", + model="Atag One", + name="Atag Thermostat", + sw_version=self.coordinator.data.apiversion, + ) diff --git a/homeassistant/components/atag/translations/bg.json b/homeassistant/components/atag/translations/bg.json new file mode 100644 index 00000000000..527adb67bf7 --- /dev/null +++ b/homeassistant/components/atag/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index f48068498c6..700b03a85da 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -33,7 +33,7 @@ API_CACHED_ATTRS = ( ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up August from a config entry.""" august_gateway = AugustGateway(hass) @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): raise ConfigEntryNotReady from err -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hass.data[DOMAIN][entry.entry_id][DATA_AUGUST].async_stop() diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 9a38cd1e301..cf34952309b 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -26,6 +26,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import callback from homeassistant.helpers.event import async_call_later @@ -129,6 +130,7 @@ SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = ( key="doorbell_online", name="Online", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_online_state, is_time_based=False, ), diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index af048f9dc46..eb7bac9ae1a 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -14,20 +14,14 @@ from .gateway import AugustGateway _LOGGER = logging.getLogger(__name__) -async def async_validate_input( - data, - august_gateway, -): +async def async_validate_input(data, august_gateway): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. Request configuration steps from the user. """ - - code = data.get(VERIFICATION_CODE_KEY) - - if code is not None: + if (code := data.get(VERIFICATION_CODE_KEY)) is not None: result = await august_gateway.authenticator.async_validate_verification_code( code ) diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index af8c858a1d4..8da7fe3d418 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -1,6 +1,6 @@ """Base class for August entity.""" from homeassistant.core import callback -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from . import DOMAIN from .const import MANUFACTURER @@ -18,14 +18,14 @@ class AugustEntityMixin(Entity): super().__init__() self._data = data self._device = device - self._attr_device_info = { - "identifiers": {(DOMAIN, self._device_id)}, - "name": device.device_name, - "manufacturer": MANUFACTURER, - "sw_version": self._detail.firmware_version, - "model": self._detail.model, - "suggested_area": _remove_device_types(device.device_name, DEVICE_TYPES), - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + manufacturer=MANUFACTURER, + model=self._detail.model, + name=device.device_name, + sw_version=self._detail.firmware_version, + suggested_area=_remove_device_types(device.device_name, DEVICE_TYPES), + ) @property def _device_id(self): diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index 5499246a187..6c9f9113d98 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -1,6 +1,7 @@ """Handle August connection setup and authentication.""" import asyncio +from http import HTTPStatus import logging import os @@ -8,12 +9,7 @@ from aiohttp import ClientError, ClientResponseError from yalexs.api_async import ApiAsync from yalexs.authenticator_async import AuthenticationState, AuthenticatorAsync -from homeassistant.const import ( - CONF_PASSWORD, - CONF_TIMEOUT, - CONF_USERNAME, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers import aiohttp_client from .const import ( @@ -97,7 +93,7 @@ class AugustGateway: # by have no access await self.api.async_get_operable_locks(self.access_token) except ClientResponseError as ex: - if ex.status == HTTP_UNAUTHORIZED: + if ex.status == HTTPStatus.UNAUTHORIZED: raise InvalidAuth from ex raise CannotConnect from ex diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index b6fa767edb7..744177cbef3 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -16,7 +16,12 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_ENTITY_PICTURE, + ENTITY_CATEGORY_DIAGNOSTIC, + PERCENTAGE, + STATE_UNAVAILABLE, +) from homeassistant.core import callback from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.restore_state import RestoreEntity @@ -68,12 +73,14 @@ class AugustSensorEntityDescription( SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail]( key="device_battery", name="Battery", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_device_battery_state, ) SENSOR_TYPE_KEYPAD_BATTERY = AugustSensorEntityDescription[KeypadDetail]( key="linked_keypad_battery", name="Battery", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_linked_keypad_battery_state, ) diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json index 22e16dda305..42f9860bdc2 100644 --- a/homeassistant/components/august/translations/hu.json +++ b/homeassistant/components/august/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -14,7 +14,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "Adja meg a(z) {username} jelszav\u00e1t.", + "description": "Adja meg {username} jelszav\u00e1t.", "title": "August fi\u00f3k \u00fajrahiteles\u00edt\u00e9se" }, "user_validate": { diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index 0c80cda4bd5..1cc378983ca 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -7,17 +7,10 @@ from aiohttp import ClientError from auroranoaa import AuroraForecast from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, -) +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -25,7 +18,6 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( - ATTR_ENTRY_TYPE, ATTRIBUTION, AURORA_API, CONF_THRESHOLD, @@ -78,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -145,12 +137,12 @@ class AuroraEntity(CoordinatorEntity): self._attr_icon = icon @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Define the device based on name.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self.unique_id)}, - ATTR_NAME: self.coordinator.name, - ATTR_MANUFACTURER: "NOAA", - ATTR_MODEL: "Aurora Visibility Sensor", - ATTR_ENTRY_TYPE: "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, str(self.unique_id))}, + manufacturer="NOAA", + model="Aurora Visibility Sensor", + name=self.coordinator.name, + ) diff --git a/homeassistant/components/aurora/const.py b/homeassistant/components/aurora/const.py index 8ce6bbad3f9..d2f91fb1222 100644 --- a/homeassistant/components/aurora/const.py +++ b/homeassistant/components/aurora/const.py @@ -3,7 +3,6 @@ DOMAIN = "aurora" COORDINATOR = "coordinator" AURORA_API = "aurora_api" -ATTR_ENTRY_TYPE = "entry_type" DEFAULT_POLLING_INTERVAL = 5 CONF_THRESHOLD = "forecast_threshold" DEFAULT_THRESHOLD = 75 diff --git a/homeassistant/components/aurora/translations/bg.json b/homeassistant/components/aurora/translations/bg.json new file mode 100644 index 00000000000..fea56662ef3 --- /dev/null +++ b/homeassistant/components/aurora/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u041f\u0440\u0430\u0433 (%)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/zh-Hant.json b/homeassistant/components/aurora/translations/zh-Hant.json index e1824a7ff4a..d12e8332373 100644 --- a/homeassistant/components/aurora/translations/zh-Hant.json +++ b/homeassistant/components/aurora/translations/zh-Hant.json @@ -22,5 +22,5 @@ } } }, - "title": "NOAA Aurora \u50b3\u611f\u5668" + "title": "NOAA Aurora \u611f\u6e2c\u5668" } \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant/components/aurora_abb_powerone/__init__.py index 087172d1bb5..2c3d0c546cd 100644 --- a/homeassistant/components/aurora_abb_powerone/__init__.py +++ b/homeassistant/components/aurora_abb_powerone/__init__.py @@ -1 +1,85 @@ """The Aurora ABB Powerone PV inverter sensor integration.""" + +# Reference info: +# https://s1.solacity.com/docs/PVI-3.0-3.6-4.2-OUTD-US%20Manual.pdf +# http://www.drhack.it/images/PDF/AuroraCommunicationProtocol_4_2.pdf +# +# Developer note: +# vscode devcontainer: use the following to access USB device: +# "runArgs": ["-e", "GIT_EDITOR=code --wait", "--device=/dev/ttyUSB0"], + +import logging + +from aurorapy.client import AuroraError, AuroraSerialClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .config_flow import validate_and_connect +from .const import ATTR_SERIAL_NUMBER, DOMAIN + +PLATFORMS = ["sensor"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Aurora ABB PowerOne from a config entry.""" + + comport = entry.data[CONF_PORT] + address = entry.data[CONF_ADDRESS] + serclient = AuroraSerialClient(address, comport, parity="N", timeout=1) + # To handle yaml import attempts in darkeness, (re)try connecting only if + # unique_id not yet assigned. + if entry.unique_id is None: + try: + res = await hass.async_add_executor_job( + validate_and_connect, hass, entry.data + ) + except AuroraError as error: + if "No response after" in str(error): + raise ConfigEntryNotReady("No response (could be dark)") from error + _LOGGER.error("Failed to connect to inverter: %s", error) + return False + except OSError as error: + if error.errno == 19: # No such device. + _LOGGER.error("Failed to connect to inverter: no such COM port") + return False + _LOGGER.error("Failed to connect to inverter: %s", error) + return False + else: + # If we got here, the device is now communicating (maybe after + # being in darkness). But there's a small risk that the user has + # configured via the UI since we last attempted the yaml setup, + # which means we'd get a duplicate unique ID. + new_id = res[ATTR_SERIAL_NUMBER] + # Check if this unique_id has already been used + for existing_entry in hass.config_entries.async_entries(DOMAIN): + if existing_entry.unique_id == new_id: + _LOGGER.debug( + "Remove already configured config entry for id %s", new_id + ) + hass.async_create_task( + hass.config_entries.async_remove(entry.entry_id) + ) + return False + hass.config_entries.async_update_entry(entry, unique_id=new_id) + + hass.data.setdefault(DOMAIN, {})[entry.unique_id] = serclient + 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) + # It should not be necessary to close the serial port because we close + # it after every use in sensor.py, i.e. no need to do entry["client"].close() + if unload_ok: + hass.data[DOMAIN].pop(entry.unique_id) + + return unload_ok diff --git a/homeassistant/components/aurora_abb_powerone/aurora_device.py b/homeassistant/components/aurora_abb_powerone/aurora_device.py new file mode 100644 index 00000000000..d2aed5ec7a8 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/aurora_device.py @@ -0,0 +1,55 @@ +"""Top level class for AuroraABBPowerOneSolarPV inverters and sensors.""" +from __future__ import annotations + +import logging + +from aurorapy.client import AuroraSerialClient + +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_DEVICE_NAME, + ATTR_FIRMWARE, + ATTR_MODEL, + ATTR_SERIAL_NUMBER, + DEFAULT_DEVICE_NAME, + DOMAIN, + MANUFACTURER, +) + +_LOGGER = logging.getLogger(__name__) + + +class AuroraDevice(Entity): + """Representation of an Aurora ABB PowerOne device.""" + + def __init__(self, client: AuroraSerialClient, data) -> None: + """Initialise the basic device.""" + self._data = data + self.type = "device" + self.client = client + self._available = True + + @property + def unique_id(self) -> str | None: + """Return the unique id for this device.""" + serial = self._data.get(ATTR_SERIAL_NUMBER) + if serial is None: + return None + return f"{serial}_{self.entity_description.key}" + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def device_info(self): + """Return device specific attributes.""" + return { + "identifiers": {(DOMAIN, self._data[ATTR_SERIAL_NUMBER])}, + "manufacturer": MANUFACTURER, + "model": self._data[ATTR_MODEL], + "name": self._data.get(ATTR_DEVICE_NAME, DEFAULT_DEVICE_NAME), + "sw_version": self._data[ATTR_FIRMWARE], + } diff --git a/homeassistant/components/aurora_abb_powerone/config_flow.py b/homeassistant/components/aurora_abb_powerone/config_flow.py new file mode 100644 index 00000000000..012fe7b14bb --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/config_flow.py @@ -0,0 +1,141 @@ +"""Config flow for Aurora ABB PowerOne integration.""" +import logging + +from aurorapy.client import AuroraError, AuroraSerialClient +import serial.tools.list_ports +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_ADDRESS, CONF_PORT + +from .const import ( + ATTR_FIRMWARE, + ATTR_MODEL, + ATTR_SERIAL_NUMBER, + DEFAULT_ADDRESS, + DEFAULT_INTEGRATION_TITLE, + DOMAIN, + MAX_ADDRESS, + MIN_ADDRESS, +) + +_LOGGER = logging.getLogger(__name__) + + +def validate_and_connect(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + comport = data[CONF_PORT] + address = data[CONF_ADDRESS] + _LOGGER.debug("Intitialising com port=%s", comport) + ret = {} + ret["title"] = DEFAULT_INTEGRATION_TITLE + try: + client = AuroraSerialClient(address, comport, parity="N", timeout=1) + client.connect() + ret[ATTR_SERIAL_NUMBER] = client.serial_number() + ret[ATTR_MODEL] = f"{client.version()} ({client.pn()})" + ret[ATTR_FIRMWARE] = client.firmware(1) + _LOGGER.info("Returning device info=%s", ret) + except AuroraError as err: + _LOGGER.warning("Could not connect to device=%s", comport) + raise err + finally: + if client.serline.isOpen(): + client.close() + + # Return info we want to store in the config entry. + return ret + + +def scan_comports(): + """Find and store available com ports for the GUI dropdown.""" + comports = serial.tools.list_ports.comports(include_links=True) + comportslist = [] + for port in comports: + comportslist.append(port.device) + _LOGGER.debug("COM port option: %s", port.device) + if len(comportslist) > 0: + return comportslist, comportslist[0] + _LOGGER.warning("No com ports found. Need a valid RS485 device to communicate") + return None, None + + +class AuroraABBConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aurora ABB PowerOne.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialise the config flow.""" + self.config = None + self._comportslist = None + self._defaultcomport = None + + async def async_step_import(self, config: dict): + """Import a configuration from config.yaml.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="already_setup") + + conf = {} + conf[CONF_PORT] = config["device"] + conf[CONF_ADDRESS] = config["address"] + # config["name"] from yaml is ignored. + + return self.async_create_entry(title=DEFAULT_INTEGRATION_TITLE, data=conf) + + async def async_step_user(self, user_input=None): + """Handle a flow initialised by the user.""" + + errors = {} + if self._comportslist is None: + result = await self.hass.async_add_executor_job(scan_comports) + self._comportslist, self._defaultcomport = result + if self._defaultcomport is None: + return self.async_abort(reason="no_serial_ports") + + # Handle the initial step. + if user_input is not None: + try: + info = await self.hass.async_add_executor_job( + validate_and_connect, self.hass, user_input + ) + info.update(user_input) + # Bomb out early if someone has already set up this device. + device_unique_id = info["serial_number"] + await self.async_set_unique_id(device_unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=info["title"], data=info) + + except OSError as error: + if error.errno == 19: # No such device. + errors["base"] = "invalid_serial_port" + except AuroraError as error: + if "could not open port" in str(error): + errors["base"] = "cannot_open_serial_port" + elif "No response after" in str(error): + errors["base"] = "cannot_connect" # could be dark + else: + _LOGGER.error( + "Unable to communicate with Aurora ABB Inverter at %s: %s %s", + user_input[CONF_PORT], + type(error), + error, + ) + errors["base"] = "cannot_connect" + # If no user input, must be first pass through the config. Show initial form. + config_options = { + vol.Required(CONF_PORT, default=self._defaultcomport): vol.In( + self._comportslist + ), + vol.Required(CONF_ADDRESS, default=DEFAULT_ADDRESS): vol.In( + range(MIN_ADDRESS, MAX_ADDRESS + 1) + ), + } + schema = vol.Schema(config_options) + + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) diff --git a/homeassistant/components/aurora_abb_powerone/const.py b/homeassistant/components/aurora_abb_powerone/const.py new file mode 100644 index 00000000000..3711dd6d800 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/const.py @@ -0,0 +1,22 @@ +"""Constants for the Aurora ABB PowerOne integration.""" + +DOMAIN = "aurora_abb_powerone" + +# Min max addresses and default according to here: +# https://library.e.abb.com/public/e57212c407344a16b4644cee73492b39/PVI-3.0_3.6_4.2-TL-OUTD-Product%20manual%20EN-RevB(M000016BG).pdf + +MIN_ADDRESS = 2 +MAX_ADDRESS = 63 +DEFAULT_ADDRESS = 2 + +DEFAULT_INTEGRATION_TITLE = "PhotoVoltaic Inverters" +DEFAULT_DEVICE_NAME = "Solar Inverter" + +DEVICES = "devices" +MANUFACTURER = "ABB" + +ATTR_DEVICE_NAME = "device_name" +ATTR_DEVICE_ID = "device_id" +ATTR_SERIAL_NUMBER = "serial_number" +ATTR_MODEL = "model" +ATTR_FIRMWARE = "firmware" diff --git a/homeassistant/components/aurora_abb_powerone/manifest.json b/homeassistant/components/aurora_abb_powerone/manifest.json index 69798ce4906..9849c0d84ee 100644 --- a/homeassistant/components/aurora_abb_powerone/manifest.json +++ b/homeassistant/components/aurora_abb_powerone/manifest.json @@ -1,8 +1,11 @@ { "domain": "aurora_abb_powerone", - "name": "Aurora ABB Solar PV", - "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone/", - "codeowners": ["@davet2001"], + "name": "Aurora ABB PowerOne Solar PV", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone", "requirements": ["aurorapy==0.2.6"], + "codeowners": [ + "@davet2001" + ], "iot_class": "local_polling" } diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index b1bcec18796..4f196c39630 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -1,6 +1,9 @@ """Support for Aurora ABB PowerOne Solar Photvoltaic (PV) inverter.""" +from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any from aurorapy.client import AuroraError, AuroraSerialClient import voluptuous as vol @@ -8,56 +11,102 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, + SensorEntityDescription, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE, CONF_NAME, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, POWER_WATT, + TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv +from .aurora_device import AuroraDevice +from .const import DEFAULT_ADDRESS, DOMAIN + _LOGGER = logging.getLogger(__name__) -DEFAULT_ADDRESS = 2 -DEFAULT_NAME = "Solar PV" +SENSOR_TYPES = [ + SensorEntityDescription( + key="instantaneouspower", + device_class=DEVICE_CLASS_POWER, + native_unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, + name="Power Output", + ), + SensorEntityDescription( + key="temp", + device_class=DEVICE_CLASS_TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + name="Temperature", + ), + SensorEntityDescription( + key="totalenergy", + device_class=DEVICE_CLASS_ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, + name="Total Energy", + ), +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_DEVICE): cv.string, vol.Optional(CONF_ADDRESS, default=DEFAULT_ADDRESS): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME, default="Solar PV"): cv.string, } ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Aurora ABB PowerOne device.""" - devices = [] - comport = config[CONF_DEVICE] - address = config[CONF_ADDRESS] - name = config[CONF_NAME] - - _LOGGER.debug("Intitialising com port=%s address=%s", comport, address) - client = AuroraSerialClient(address, comport, parity="N", timeout=1) - - devices.append(AuroraABBSolarPVMonitorSensor(client, name, "Power")) - add_entities(devices, True) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up based on configuration.yaml (DEPRECATED).""" + _LOGGER.warning( + "Loading aurora_abb_powerone via platform config is deprecated; The configuration" + " has been migrated to a config entry and can be safely removed from configuration.yaml" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) -class AuroraABBSolarPVMonitorSensor(SensorEntity): - """Representation of a Sensor.""" +async def async_setup_entry(hass, config_entry, async_add_entities) -> None: + """Set up aurora_abb_powerone sensor based on a config entry.""" + entities = [] - _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_native_unit_of_measurement = POWER_WATT - _attr_device_class = DEVICE_CLASS_POWER + client = hass.data[DOMAIN][config_entry.unique_id] + data = config_entry.data - def __init__(self, client, name, typename): + for sens in SENSOR_TYPES: + entities.append(AuroraSensor(client, data, sens)) + + _LOGGER.debug("async_setup_entry adding %d entities", len(entities)) + async_add_entities(entities, True) + + +class AuroraSensor(AuroraDevice, SensorEntity): + """Representation of a Sensor on a Aurora ABB PowerOne Solar inverter.""" + + def __init__( + self, + client: AuroraSerialClient, + data: Mapping[str, Any], + entity_description: SensorEntityDescription, + ) -> None: """Initialize the sensor.""" - self._attr_name = f"{name} {typename}" - self.client = client + super().__init__(client, data) + self.entity_description = entity_description + self.availableprev = True def update(self): """Fetch new state data for the sensor. @@ -65,11 +114,24 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): This is the only method that should fetch new data for Home Assistant. """ try: + self.availableprev = self._attr_available self.client.connect() - # read ADC channel 3 (grid power output) - power_watts = self.client.measure(3, True) - self._attr_native_value = round(power_watts, 1) + if self.entity_description.key == "instantaneouspower": + # read ADC channel 3 (grid power output) + power_watts = self.client.measure(3, True) + self._attr_native_value = round(power_watts, 1) + elif self.entity_description.key == "temp": + temperature_c = self.client.measure(21) + self._attr_native_value = round(temperature_c, 1) + elif self.entity_description.key == "totalenergy": + energy_wh = self.client.cumulated_energy(5) + self._attr_native_value = round(energy_wh / 1000, 2) + self._attr_available = True + except AuroraError as error: + self._attr_state = None + self._attr_native_value = None + self._attr_available = False # aurorapy does not have different exceptions (yet) for dealing # with timeout vs other comms errors. # This means the (normal) situation of no response during darkness @@ -82,7 +144,14 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): _LOGGER.debug("No response from inverter (could be dark)") else: raise error - self._attr_native_value = None finally: + if self._attr_available != self.availableprev: + if self._attr_available: + _LOGGER.info("Communication with %s back online", self.name) + else: + _LOGGER.warning( + "Communication with %s lost", + self.name, + ) if self.client.serline.isOpen(): self.client.close() diff --git a/homeassistant/components/aurora_abb_powerone/strings.json b/homeassistant/components/aurora_abb_powerone/strings.json new file mode 100644 index 00000000000..b705c5f69a5 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "description": "The inverter must be connected via an RS485 adaptor, please select serial port and the inverter's address as configured on the LCD panel", + "data": { + "port": "RS485 or USB-RS485 Adaptor Port", + "address": "Inverter Address" + } + } + }, + "error": { + "cannot_connect": "Unable to connect, please check serial port, address, electrical connection and that inverter is on (in daylight)", + "invalid_serial_port": "Serial port is not a valid device or could not be openned", + "cannot_open_serial_port": "Cannot open serial port, please check and try again", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "Device is already configured", + "no_serial_ports": "No com ports found. Need a valid RS485 device to communicate." + } + } +} diff --git a/homeassistant/components/aurora_abb_powerone/translations/ca.json b/homeassistant/components/aurora_abb_powerone/translations/ca.json new file mode 100644 index 00000000000..6976430a9b3 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "no_serial_ports": "No s'han trobat ports COM. Es necessita un dispositiu de comunicaci\u00f3 RS485 v\u00e0lid." + }, + "error": { + "cannot_open_serial_port": "No s'ha pogut obrir el port s\u00e8rie, comprova'l i torna-ho a provar", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "address": "Adre\u00e7a de l'inversor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/de.json b/homeassistant/components/aurora_abb_powerone/translations/de.json new file mode 100644 index 00000000000..a60138da16d --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_serial_ports": "Keine COM-Ports gefunden. Man ben\u00f6tigt ein g\u00fcltiges RS485-Ger\u00e4t, um zu kommunizieren." + }, + "error": { + "cannot_connect": "Verbindung kann nicht hergestellt werden, bitte \u00fcberpr\u00fcfe den seriellen Anschluss, die Adresse, die elektrische Verbindung und ob der Wechselrichter eingeschaltet ist (bei Tageslicht)", + "cannot_open_serial_port": "Serielle Schnittstelle kann nicht ge\u00f6ffnet werden, bitte pr\u00fcfen und erneut versuchen", + "invalid_serial_port": "Serielle Schnittstelle ist kein g\u00fcltiges Ger\u00e4t oder konnte nicht ge\u00f6ffnet werden", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "address": "Wechselrichter Adresse", + "port": "RS485- oder USB-RS485-Adapteranschluss" + }, + "description": "Der Wechselrichter muss \u00fcber einen RS485-Adapter angeschlossen werden, bitte w\u00e4hle die serielle Schnittstelle und die Adresse des Wechselrichters wie auf dem LCD-Panel konfiguriert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/en.json b/homeassistant/components/aurora_abb_powerone/translations/en.json new file mode 100644 index 00000000000..fe5d668f573 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "no_serial_ports": "No com ports found. Need a valid RS485 device to communicate." + }, + "error": { + "cannot_connect": "Unable to connect, please check serial port, address, electrical connection and that inverter is on (in daylight)", + "cannot_open_serial_port": "Cannot open serial port, please check and try again", + "invalid_serial_port": "Serial port is not a valid device or could not be openned", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "address": "Inverter Address", + "port": "RS485 or USB-RS485 Adaptor Port" + }, + "description": "The inverter must be connected via an RS485 adaptor, please select serial port and the inverter's address as configured on the LCD panel" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/en_GB.json b/homeassistant/components/aurora_abb_powerone/translations/en_GB.json new file mode 100644 index 00000000000..59c6263bd14 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/en_GB.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "address": "Inverter Address" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/et.json b/homeassistant/components/aurora_abb_powerone/translations/et.json new file mode 100644 index 00000000000..b7764fa0f33 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "no_serial_ports": "Jadaporte ei leitud. Suhtlemiseks on vaja kehtivat RS485 seadet." + }, + "error": { + "cannot_connect": "\u00dchendust ei saa luua, palun kontrolli jadaporti, aadressi, elektri\u00fchendust ja et inverter on sisse l\u00fclitatud (p\u00e4evavalguses)", + "cannot_open_serial_port": "Jadaporti ei saa avada, kontrolli ja proovi uuesti", + "invalid_serial_port": "Jadaport pole sobiv seade v\u00f5i seda ei saa avada", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "address": "Inverteri aadress", + "port": "RS485 v\u00f5i USB-RS485 adapteri port" + }, + "description": "Inverter peab olema \u00fchendatud RS485 adapteri kaudu, vali jadaport ja muunduri aadress nagu on konfigureeritud LCD paneelil" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/hu.json b/homeassistant/components/aurora_abb_powerone/translations/hu.json new file mode 100644 index 00000000000..ffc812dfd4b --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_serial_ports": "Nem tal\u00e1lhat\u00f3 soros port. A kommunik\u00e1ci\u00f3hoz egy \u00e9rv\u00e9nyes RS485-\u00f6s csatlakoz\u00e1si lehet\u0151s\u00e9gre van sz\u00fcks\u00e9g." + }, + "error": { + "cannot_connect": "A csatlakoz\u00e1s sikertelen. Ellen\u0151rizze a soros portot, a c\u00edmet, az elektromos csatlakoz\u00e1st \u00e9s azt, hogy az inverter be van-e kapcsolva (nappal).", + "cannot_open_serial_port": "A soros port nem nyithat\u00f3 meg, k\u00e9rem ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lkozzon \u00fajra", + "invalid_serial_port": "A soros port nem \u00e9rv\u00e9nyes eszk\u00f6z, vagy nem nyithat\u00f3 meg", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "address": "Inverter c\u00edm", + "port": "RS485 vagy USB-RS485 adapter port" + }, + "description": "Az invertert RS485 adapteren kereszt\u00fcl kell csatlakoztatni, v\u00e1lassza ki a soros portot \u00e9s az inverter c\u00edm\u00e9t az LCD panelen konfigur\u00e1ltak szerint." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/pl.json b/homeassistant/components/aurora_abb_powerone/translations/pl.json new file mode 100644 index 00000000000..c931afdae8d --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/pl.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "unknown": "Nieoczekiwany b\u0142\u0105d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/ru.json b/homeassistant/components/aurora_abb_powerone/translations/ru.json new file mode 100644 index 00000000000..6baec9324d2 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/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.", + "no_serial_ports": "COM-\u043f\u043e\u0440\u0442\u044b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b. \u0414\u043b\u044f \u0441\u0432\u044f\u0437\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e RS485." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442, \u0430\u0434\u0440\u0435\u0441, \u044d\u043b\u0435\u043a\u0442\u0440\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0438 \u0447\u0442\u043e \u0438\u043d\u0432\u0435\u0440\u0442\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d (\u043f\u0440\u0438 \u0434\u043d\u0435\u0432\u043d\u043e\u043c \u0441\u0432\u0435\u0442\u0435).", + "cannot_open_serial_port": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_serial_port": "\u041f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u043c \u0438\u043b\u0438 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 \u0438\u043d\u0432\u0435\u0440\u0442\u043e\u0440\u0430", + "port": "\u041f\u043e\u0440\u0442 \u0430\u0434\u0430\u043f\u0442\u0435\u0440\u0430 RS485 \u0438\u043b\u0438 USB-RS485" + }, + "description": "\u0418\u043d\u0432\u0435\u0440\u0442\u043e\u0440 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u0447\u0435\u0440\u0435\u0437 \u0430\u0434\u0430\u043f\u0442\u0435\u0440 RS485. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0438 \u0430\u0434\u0440\u0435\u0441 \u0438\u043d\u0432\u0435\u0440\u0442\u043e\u0440\u0430, \u043a\u0430\u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0438\u043d\u0432\u0435\u0440\u0442\u043e\u0440\u0430." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/zh-Hant.json b/homeassistant/components/aurora_abb_powerone/translations/zh-Hant.json new file mode 100644 index 00000000000..0a05e640994 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_serial_ports": "\u627e\u4e0d\u5230\u901a\u8a0a\u57e0\u3002\u9700\u8981\u6709\u6548\u7684 RS485 \u88dd\u7f6e\u9032\u884c\u901a\u8a0a\u3002" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u63a5\uff0c\u8acb\u6aa2\u67e5\u5e8f\u5217\u57e0\u3001\u4f4d\u5740\u3001\u96fb\u529b\u9023\u63a5\uff0c\u4e26\u78ba\u5b9a\u8a72\u8b8a\u6d41\u5668\u70ba\u958b\u555f\u72c0\u614b\uff08\u767d\u5929\uff09", + "cannot_open_serial_port": "\u7121\u6cd5\u958b\u555f\u5e8f\u5217\u57e0\u3001\u8acb\u6aa2\u67e5\u5f8c\u518d\u8a66\u4e00\u6b21", + "invalid_serial_port": "\u5e8f\u5217\u57e0\u70ba\u7121\u6548\u88dd\u7f6e\u6216\u7121\u6cd5\u958b\u555f", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "address": "\u8b8a\u6d41\u5668\u4f4d\u5740", + "port": "RS485 \u6216 USB-RS485 \u8f49\u63a5\u5668\u901a\u8a0a\u57e0" + }, + "description": "\u8b8a\u6d41\u5668\u5fc5\u9808\u900f\u904e RS485 \u8f49\u63a5\u5668\u9032\u884c\u9023\u63a5\u3001\u8acb\u9078\u64c7 LCD \u756b\u9762\u4e0a\u6240\u8a2d\u5b9a\u7684\u5e8f\u5217\u57e0\u53ca\u8b8a\u6d41\u5668\u4f4d\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 49c18b4737a..bcdcf4de747 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -134,7 +134,6 @@ from homeassistant.components.http.auth import async_sign_path from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import HTTP_OK from homeassistant.core import HomeAssistant, callback from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -271,18 +270,16 @@ class TokenView(HomeAssistantView): # 2.2 The authorization server responds with HTTP status code 200 # if the token has been revoked successfully or if the client # submitted an invalid token. - token = data.get("token") - - if token is None: - return web.Response(status=HTTP_OK) + if (token := data.get("token")) is None: + return web.Response(status=HTTPStatus.OK) refresh_token = await hass.auth.async_get_refresh_token_by_token(token) if refresh_token is None: - return web.Response(status=HTTP_OK) + return web.Response(status=HTTPStatus.OK) await hass.auth.async_remove_refresh_token(refresh_token) - return web.Response(status=HTTP_OK) + return web.Response(status=HTTPStatus.OK) async def _async_handle_auth_code(self, hass, data, remote_addr): """Handle authorization code request.""" @@ -293,9 +290,7 @@ class TokenView(HomeAssistantView): status_code=HTTPStatus.BAD_REQUEST, ) - code = data.get("code") - - if code is None: + if (code := data.get("code")) is None: return self.json( {"error": "invalid_request", "error_description": "Invalid code"}, status_code=HTTPStatus.BAD_REQUEST, @@ -350,9 +345,7 @@ class TokenView(HomeAssistantView): status_code=HTTPStatus.BAD_REQUEST, ) - token = data.get("refresh_token") - - if token is None: + if (token := data.get("refresh_token")) is None: return self.json( {"error": "invalid_request"}, status_code=HTTPStatus.BAD_REQUEST ) @@ -413,7 +406,15 @@ class LinkUserView(HomeAssistantView): if credentials is None: return self.json_message("Invalid code", status_code=HTTPStatus.BAD_REQUEST) - await hass.auth.async_link_user(user, credentials) + linked_user = await hass.auth.async_get_user_by_credentials(credentials) + if linked_user != user and linked_user is not None: + return self.json_message( + "Credential already linked", status_code=HTTPStatus.BAD_REQUEST + ) + + # No-op if credential is already linked to the user it will be linked to + if linked_user != user: + await hass.auth.async_link_user(user, credentials) return self.json_message("User linked") diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index f15eeee2f16..ed5c544499e 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -81,7 +81,6 @@ from homeassistant.components.http.ban import ( ) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import HTTP_METHOD_NOT_ALLOWED from . import indieauth @@ -131,8 +130,7 @@ def _prepare_result_json(result): data = result.copy() - schema = data["data_schema"] - if schema is None: + if (schema := data["data_schema"]) is None: data["data_schema"] = [] else: data["data_schema"] = voluptuous_serialize.convert(schema) @@ -155,7 +153,7 @@ class LoginFlowIndexView(HomeAssistantView): async def get(self, request): """Do not allow index of flows in progress.""" # pylint: disable=no-self-use - return web.Response(status=HTTP_METHOD_NOT_ALLOWED) + return web.Response(status=HTTPStatus.METHOD_NOT_ALLOWED) @RequestDataValidator( vol.Schema( @@ -233,14 +231,9 @@ class LoginFlowResourceView(HomeAssistantView): try: # do not allow change ip during login flow - for flow in self._flow_mgr.async_progress(): - if flow["flow_id"] == flow_id and flow["context"][ - "ip_address" - ] != ip_address(request.remote): - return self.json_message( - "IP address changed", HTTPStatus.BAD_REQUEST - ) - + flow = self._flow_mgr.async_get(flow_id) + if flow["context"]["ip_address"] != ip_address(request.remote): + return self.json_message("IP address changed", HTTPStatus.BAD_REQUEST) result = await self._flow_mgr.async_configure(flow_id, data) except data_entry_flow.UnknownFlow: return self.json_message("Invalid flow specified", HTTPStatus.NOT_FOUND) diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 1b199551a14..61c06a3c16e 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -70,8 +70,7 @@ def websocket_setup_mfa( """Return a setup flow for mfa auth module.""" flow_manager = hass.data[DATA_SETUP_FLOW_MGR] - flow_id = msg.get("flow_id") - if flow_id is not None: + if (flow_id := msg.get("flow_id")) is not None: result = await flow_manager.async_configure(flow_id, msg.get("user_input")) connection.send_message( websocket_api.result_message(msg["id"], _prepare_result_json(result)) @@ -139,8 +138,7 @@ def _prepare_result_json(result): data = result.copy() - schema = data["data_schema"] - if schema is None: + if (schema := data["data_schema"]) is None: data["data_schema"] = [] else: data["data_schema"] = voluptuous_serialize.convert(schema) diff --git a/homeassistant/components/auth/translations/hu.json b/homeassistant/components/auth/translations/hu.json index 47ecf846e0f..99504c1b7a7 100644 --- a/homeassistant/components/auth/translations/hu.json +++ b/homeassistant/components/auth/translations/hu.json @@ -13,7 +13,7 @@ "title": "\u00c1ll\u00edtsa be az \u00e9rtes\u00edt\u00e9si \u00f6sszetev\u0151 \u00e1ltal megadott egyszeri jelsz\u00f3t" }, "setup": { - "description": "Az egyszeri jelsz\u00f3 el lett k\u00fcldve a(z) **notify.{notify_service}** szolg\u00e1ltat\u00e1ssal. K\u00e9rem, adja meg al\u00e1bb:", + "description": "Az egyszeri jelsz\u00f3 el lett k\u00fcldve **notify.{notify_service}** szolg\u00e1ltat\u00e1ssal. K\u00e9rem, adja meg al\u00e1bb:", "title": "Be\u00e1ll\u00edt\u00e1s ellen\u0151rz\u00e9se" } }, diff --git a/homeassistant/components/auth/translations/ja.json b/homeassistant/components/auth/translations/ja.json new file mode 100644 index 00000000000..1ef902e6fe2 --- /dev/null +++ b/homeassistant/components/auth/translations/ja.json @@ -0,0 +1,7 @@ +{ + "mfa_setup": { + "totp": { + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 24090b79fa8..92fbd0e8b04 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -228,7 +228,6 @@ def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: async def async_setup(hass, config): """Set up all automations.""" - # Local import to avoid circular import hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) # To register the automation blueprints @@ -460,8 +459,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._trace_config, ) as automation_trace: this = None - state = self.hass.states.get(self.entity_id) - if state: + if state := self.hass.states.get(self.entity_id): this = state.as_dict() variables = {"this": this, **(run_variables or {})} if self._variables: @@ -589,8 +587,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): this = None self.async_write_ha_state() - state = self.hass.states.get(self.entity_id) - if state: + if state := self.hass.states.get(self.entity_id): this = state.as_dict() variables = {"this": this} if self._trigger_variables: diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py index dd2ba824f8a..4318cdafa39 100644 --- a/homeassistant/components/automation/reproduce_state.py +++ b/homeassistant/components/automation/reproduce_state.py @@ -30,9 +30,7 @@ async def _async_reproduce_state( 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: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index 1fbc7e5cbc9..f76dd57e4ed 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -8,6 +8,8 @@ from homeassistant.components.trace import ActionTrace, async_store_trace from homeassistant.components.trace.const import CONF_STORED_TRACES from homeassistant.core import Context +from .const import DOMAIN + # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -15,6 +17,8 @@ from homeassistant.core import Context class AutomationTrace(ActionTrace): """Container for automation trace.""" + _domain = DOMAIN + def __init__( self, item_id: str, @@ -23,8 +27,7 @@ class AutomationTrace(ActionTrace): context: Context, ) -> None: """Container for automation trace.""" - key = ("automation", item_id) - super().__init__(key, config, blueprint_inputs, context) + super().__init__(item_id, config, blueprint_inputs, context) self._trigger_description: str | None = None def set_trigger_description(self, trigger: str) -> None: @@ -33,6 +36,9 @@ class AutomationTrace(ActionTrace): def as_short_dict(self) -> dict[str, Any]: """Return a brief dictionary version of this AutomationTrace.""" + if self._short_dict: + return self._short_dict + result = super().as_short_dict() result["trigger"] = self._trigger_description return result diff --git a/homeassistant/components/automation/translations/ca.json b/homeassistant/components/automation/translations/ca.json index c1d35331e2b..7d96a6a466d 100644 --- a/homeassistant/components/automation/translations/ca.json +++ b/homeassistant/components/automation/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Automatitzaci\u00f3" diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py index fcc780f77bc..0bf1787aac7 100644 --- a/homeassistant/components/avion/light.py +++ b/homeassistant/components/avion/light.py @@ -95,9 +95,7 @@ class AvionLight(LightEntity): def turn_on(self, **kwargs): """Turn the specified or all lights on.""" - brightness = kwargs.get(ATTR_BRIGHTNESS) - - if brightness is not None: + if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: self._attr_brightness = brightness self.set_state(self.brightness) diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 80591e36f2d..1ff1b6e0efb 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -7,7 +7,12 @@ import voluptuous as vol from homeassistant.components.awair import AwairDataUpdateCoordinator, AwairResult from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_CONNECTIONS, + ATTR_NAME, + CONF_ACCESS_TOKEN, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv @@ -211,17 +216,17 @@ class AwairSensor(CoordinatorEntity, SensorEntity): @property def device_info(self) -> DeviceInfo: """Device information.""" - info = { - "identifiers": {(DOMAIN, self._device.uuid)}, - "manufacturer": "Awair", - "model": self._device.model, - } + info = DeviceInfo( + identifiers={(DOMAIN, self._device.uuid)}, + manufacturer="Awair", + model=self._device.model, + ) if self._device.name: - info["name"] = self._device.name + info[ATTR_NAME] = self._device.name if self._device.mac_address: - info["connections"] = { + info[ATTR_CONNECTIONS] = { (dr.CONNECTION_NETWORK_MAC, self._device.mac_address) } diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index 62187becd37..e3994430a8b 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index da8c27d7445..6dcbad748cc 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -85,8 +85,7 @@ async def async_setup(hass, config): """Set up AWS component.""" hass.data[DATA_HASS_CONFIG] = config - conf = config.get(DOMAIN) - if conf is None: + if (conf := config.get(DOMAIN)) is None: # create a default conf using default profile conf = CONFIG_SCHEMA({ATTR_CREDENTIALS: DEFAULT_CREDENTIAL}) @@ -159,9 +158,7 @@ async def _validate_aws_credentials(hass, credential): del aws_config[CONF_NAME] del aws_config[CONF_VALIDATE] - profile = aws_config.get(CONF_PROFILE_NAME) - - if profile is not None: + if (profile := aws_config.get(CONF_PROFILE_NAME)) is not None: session = aiobotocore.AioSession(profile=profile) del aws_config[CONF_PROFILE_NAME] if CONF_ACCESS_KEY_ID in aws_config: diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index c9d6ca2faa7..b271a2a8786 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -82,8 +82,7 @@ async def async_get_service(hass, config, discovery_info=None): del aws_config[CONF_CREDENTIAL_NAME] if session is None: - profile = aws_config.get(CONF_PROFILE_NAME) - if profile is not None: + if (profile := aws_config.get(CONF_PROFILE_NAME)) is not None: session = aiobotocore.AioSession(profile=profile) del aws_config[CONF_PROFILE_NAME] else: diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index a652aeb6df8..791764dc605 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -1,9 +1,8 @@ """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 +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as AXIS_DOMAIN @@ -15,7 +14,9 @@ class AxisEntityBase(Entity): """Initialize the Axis event.""" self.device = device - self._attr_device_info = {ATTR_IDENTIFIERS: {(AXIS_DOMAIN, device.unique_id)}} + self._attr_device_info = DeviceInfo( + identifiers={(AXIS_DOMAIN, device.unique_id)} + ) async def async_added_to_hass(self): """Subscribe device events.""" diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 90eacf47965..823593ecacb 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -23,6 +23,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.httpx_client import get_async_client @@ -168,9 +169,10 @@ class AxisNetworkDevice: async def async_update_device_registry(self): """Update device registry.""" - device_registry = await self.hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(self.hass) device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, + configuration_url=self.api.config.url, connections={(CONNECTION_NETWORK_MAC, self.unique_id)}, identifiers={(AXIS_DOMAIN, self.unique_id)}, manufacturer=ATTR_MANUFACTURER, diff --git a/homeassistant/components/axis/translations/hu.json b/homeassistant/components/axis/translations/hu.json index 0cddf167437..cb2f9a17c93 100644 --- a/homeassistant/components/axis/translations/hu.json +++ b/homeassistant/components/axis/translations/hu.json @@ -7,7 +7,7 @@ }, "error": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 39307ac41df..fe27ec8bcec 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -83,15 +83,9 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this Azure DevOps instance.""" - return { - "identifiers": { - ( # type: ignore - DOMAIN, - self.organization, - self.project, - ) - }, - "manufacturer": self.organization, - "name": self.project, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self.organization, self.project)}, # type: ignore + manufacturer=self.organization, + name=self.project, + ) diff --git a/homeassistant/components/azure_devops/translations/bg.json b/homeassistant/components/azure_devops/translations/bg.json new file mode 100644 index 00000000000..d9f03d82592 --- /dev/null +++ b/homeassistant/components/azure_devops/translations/bg.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + } + } +} \ 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 e42ebc8d8e2..2d8879b9d68 100644 --- a/homeassistant/components/azure_devops/translations/hu.json +++ b/homeassistant/components/azure_devops/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -15,7 +15,7 @@ "data": { "personal_access_token": "Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si token (PAT)" }, - "description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", + "description": "{project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", "title": "\u00dajrahiteles\u00edt\u00e9s" }, "user": { diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index e7c85adede8..0d48ff6b2d6 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -90,8 +90,7 @@ class ServiceBusNotificationService(BaseNotificationService): if ATTR_TARGET in kwargs: dto[ATTR_ASB_TARGET] = kwargs[ATTR_TARGET] - data = kwargs.get(ATTR_DATA) - if data: + if data := kwargs.get(ATTR_DATA): dto.update(data) queue_message = Message(json.dumps(dto)) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index d2c06f45875..53a7e8720b1 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -10,11 +10,11 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_MONITORED_VARIABLES, CONF_NAME, DATA_RATE_MEGABITS_PER_SECOND, @@ -49,12 +49,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="current_down_bandwidth", name="Currently Used Download Bandwidth", native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + state_class=STATE_CLASS_MEASUREMENT, icon="mdi:download", ), SensorEntityDescription( key="current_up_bandwidth", name="Currently Used Upload Bandwidth", native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + state_class=STATE_CLASS_MEASUREMENT, icon="mdi:upload", ), SensorEntityDescription( @@ -117,7 +119,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BboxUptimeSensor(SensorEntity): """Bbox uptime sensor.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION _attr_device_class = DEVICE_CLASS_TIMESTAMP def __init__(self, bbox_data, name, description: SensorEntityDescription): @@ -138,7 +140,7 @@ class BboxUptimeSensor(SensorEntity): class BboxSensor(SensorEntity): """Implementation of a Bbox sensor.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__(self, bbox_data, name, description: SensorEntityDescription): """Initialize the sensor.""" diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 87d574fc4b0..aff7f9a3135 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -83,6 +83,9 @@ DEVICE_CLASS_PRESENCE = "presence" # On means problem detected, Off means no problem (OK) DEVICE_CLASS_PROBLEM = "problem" +# On means running, Off means not running +DEVICE_CLASS_RUNNING = "running" + # On means unsafe, Off means safe DEVICE_CLASS_SAFETY = "safety" @@ -92,6 +95,9 @@ DEVICE_CLASS_SMOKE = "smoke" # On means sound detected, Off means no sound (clear) DEVICE_CLASS_SOUND = "sound" +# On means tampering detected, Off means no tampering (clear) +DEVICE_CLASS_TAMPER = "tamper" + # On means update available, Off means up-to-date DEVICE_CLASS_UPDATE = "update" @@ -121,9 +127,11 @@ DEVICE_CLASSES = [ DEVICE_CLASS_POWER, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index a6b9d3ffb8b..8351234182d 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -34,9 +34,11 @@ from . import ( DEVICE_CLASS_POWER, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, @@ -79,12 +81,16 @@ CONF_IS_PRESENT = "is_present" CONF_IS_NOT_PRESENT = "is_not_present" CONF_IS_PROBLEM = "is_problem" CONF_IS_NO_PROBLEM = "is_no_problem" +CONF_IS_RUNNING = "is_running" +CONF_IS_NOT_RUNNING = "is_not_running" CONF_IS_UNSAFE = "is_unsafe" CONF_IS_NOT_UNSAFE = "is_not_unsafe" CONF_IS_SMOKE = "is_smoke" CONF_IS_NO_SMOKE = "is_no_smoke" CONF_IS_SOUND = "is_sound" CONF_IS_NO_SOUND = "is_no_sound" +CONF_IS_TAMPERED = "is_tampered" +CONF_IS_NOT_TAMPERED = "is_not_tampered" CONF_IS_UPDATE = "is_update" CONF_IS_NO_UPDATE = "is_no_update" CONF_IS_VIBRATION = "is_vibration" @@ -110,8 +116,10 @@ IS_ON = [ CONF_IS_POWERED, CONF_IS_PRESENT, CONF_IS_PROBLEM, + CONF_IS_RUNNING, CONF_IS_SMOKE, CONF_IS_SOUND, + CONF_IS_TAMPERED, CONF_IS_UPDATE, CONF_IS_UNSAFE, CONF_IS_VIBRATION, @@ -132,11 +140,13 @@ IS_OFF = [ CONF_IS_NOT_PLUGGED_IN, CONF_IS_NOT_POWERED, CONF_IS_NOT_PRESENT, + CONF_IS_NOT_TAMPERED, CONF_IS_NOT_UNSAFE, CONF_IS_NO_GAS, CONF_IS_NO_LIGHT, CONF_IS_NO_MOTION, CONF_IS_NO_PROBLEM, + CONF_IS_NOT_RUNNING, CONF_IS_NO_SMOKE, CONF_IS_NO_SOUND, CONF_IS_NO_UPDATE, @@ -191,9 +201,17 @@ ENTITY_CONDITIONS = { {CONF_TYPE: CONF_IS_PROBLEM}, {CONF_TYPE: CONF_IS_NO_PROBLEM}, ], + DEVICE_CLASS_RUNNING: [ + {CONF_TYPE: CONF_IS_RUNNING}, + {CONF_TYPE: CONF_IS_NOT_RUNNING}, + ], DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], + DEVICE_CLASS_TAMPER: [ + {CONF_TYPE: CONF_IS_TAMPERED}, + {CONF_TYPE: CONF_IS_NOT_TAMPERED}, + ], DEVICE_CLASS_UPDATE: [{CONF_TYPE: CONF_IS_UPDATE}, {CONF_TYPE: CONF_IS_NO_UPDATE}], DEVICE_CLASS_VIBRATION: [ {CONF_TYPE: CONF_IS_VIBRATION}, diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index a0966b5a018..72cd885d467 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -32,9 +32,11 @@ from . import ( DEVICE_CLASS_POWER, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, @@ -77,12 +79,16 @@ CONF_PRESENT = "present" CONF_NOT_PRESENT = "not_present" CONF_PROBLEM = "problem" CONF_NO_PROBLEM = "no_problem" +CONF_RUNNING = "running" +CONF_NOT_RUNNING = "not_running" CONF_UNSAFE = "unsafe" CONF_NOT_UNSAFE = "not_unsafe" CONF_SMOKE = "smoke" CONF_NO_SMOKE = "no_smoke" CONF_SOUND = "sound" CONF_NO_SOUND = "no_sound" +CONF_TAMPERED = "tampered" +CONF_NOT_TAMPERED = "not_tampered" CONF_UPDATE = "update" CONF_NO_UPDATE = "no_update" CONF_VIBRATION = "vibration" @@ -108,11 +114,13 @@ TURNED_ON = [ CONF_POWERED, CONF_PRESENT, CONF_PROBLEM, + CONF_RUNNING, CONF_SMOKE, CONF_SOUND, CONF_UNSAFE, CONF_UPDATE, CONF_VIBRATION, + CONF_TAMPERED, CONF_TURNED_ON, ] @@ -129,11 +137,13 @@ TURNED_OFF = [ CONF_NOT_PLUGGED_IN, CONF_NOT_POWERED, CONF_NOT_PRESENT, + CONF_NOT_TAMPERED, CONF_NOT_UNSAFE, CONF_NO_GAS, CONF_NO_LIGHT, CONF_NO_MOTION, CONF_NO_PROBLEM, + CONF_NOT_RUNNING, CONF_NO_SMOKE, CONF_NO_SOUND, CONF_NO_VIBRATION, @@ -170,10 +180,12 @@ ENTITY_TRIGGERS = { DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}], + DEVICE_CLASS_RUNNING: [{CONF_TYPE: CONF_RUNNING}, {CONF_TYPE: CONF_NOT_RUNNING}], DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], DEVICE_CLASS_UPDATE: [{CONF_TYPE: CONF_UPDATE}, {CONF_TYPE: CONF_NO_UPDATE}], + DEVICE_CLASS_TAMPER: [{CONF_TYPE: CONF_TAMPERED}, {CONF_TYPE: CONF_NOT_TAMPERED}], DEVICE_CLASS_VIBRATION: [ {CONF_TYPE: CONF_VIBRATION}, {CONF_TYPE: CONF_NO_VIBRATION}, diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 62b6ec20323..eb97b370105 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -32,12 +32,16 @@ "is_not_present": "{entity_name} is not present", "is_problem": "{entity_name} is detecting problem", "is_no_problem": "{entity_name} is not detecting problem", + "is_running": "{entity_name} is running", + "is_not_running": "{entity_name} is not running", "is_unsafe": "{entity_name} is unsafe", "is_not_unsafe": "{entity_name} is safe", "is_smoke": "{entity_name} is detecting smoke", "is_no_smoke": "{entity_name} is not detecting smoke", "is_sound": "{entity_name} is detecting sound", "is_no_sound": "{entity_name} is not detecting sound", + "is_tampered": "{entity_name} is detecting tampering", + "is_not_tampered": "{entity_name} is not detecting tampering", "is_update": "{entity_name} has an update available", "is_no_update": "{entity_name} is up-to-date", "is_vibration": "{entity_name} is detecting vibration", @@ -78,12 +82,16 @@ "not_present": "{entity_name} not present", "problem": "{entity_name} started detecting problem", "no_problem": "{entity_name} stopped detecting problem", + "running": "{entity_name} started running", + "not_running": "{entity_name} is no longer running", "unsafe": "{entity_name} became unsafe", "not_unsafe": "{entity_name} became safe", "smoke": "{entity_name} started detecting smoke", "no_smoke": "{entity_name} stopped detecting smoke", "sound": "{entity_name} started detecting sound", "no_sound": "{entity_name} stopped detecting sound", + "is_tampered": "{entity_name} started detecting tampering", + "is_not_tampered": "{entity_name} stopped detecting tampering", "update": "{entity_name} got an update available", "no_update": "{entity_name} became up-to-date", "vibration": "{entity_name} started detecting vibration", @@ -167,6 +175,10 @@ "off": "OK", "on": "Problem" }, + "running": { + "off": "Not running", + "on": "Running" + }, "safety": { "off": "Safe", "on": "Unsafe" @@ -195,5 +207,18 @@ "off": "[%key:common::state::off%]", "on": "[%key:common::state::on%]" } + }, + "device_class": { + "cold": "cold", + "gas": "gas", + "heat": "heat", + "moisture": "moisture", + "motion": "motion", + "occupancy": "occupancy", + "power": "power", + "problem": "problem", + "smoke": "smoke", + "sound": "sound", + "vibration": "vibration" } -} +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/bg.json b/homeassistant/components/binary_sensor/translations/bg.json index 2d969af731e..b1b3d766dc4 100644 --- a/homeassistant/components/binary_sensor/translations/bg.json +++ b/homeassistant/components/binary_sensor/translations/bg.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", "is_no_sound": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0437\u0432\u0443\u043a", + "is_no_update": "{entity_name} \u0435 \u0430\u043a\u0442\u0443\u0430\u043b\u0435\u043d", "is_no_vibration": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438", "is_not_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0435 \u0437\u0430\u0440\u0435\u0434\u0435\u043d\u0430", "is_not_cold": "{entity_name} \u043d\u0435 \u0435 \u0441\u0442\u0443\u0434\u0435\u043d", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", "is_sound": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0437\u0432\u0443\u043a", "is_unsafe": "{entity_name} \u043d\u0435 \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "is_update": "{entity_name} \u0438\u043c\u0430 \u043d\u0430\u043b\u0438\u0447\u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f", "is_vibration": "{entity_name} \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", "no_smoke": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", "no_sound": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0437\u0432\u0443\u043a", + "no_update": "{entity_name} \u0435 \u0430\u043a\u0442\u0443\u0430\u043b\u0435\u043d", "no_vibration": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438", "not_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u043d\u0435 \u0435 \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430", "not_cold": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", "turned_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", "unsafe": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u043e\u043f\u0430\u0441\u0435\u043d", + "update": "{entity_name} \u0438\u043c\u0430 \u043d\u0430\u043b\u0438\u0447\u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f", "vibration": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" } }, @@ -162,6 +166,10 @@ "off": "\u0427\u0438\u0441\u0442\u043e", "on": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d" }, + "update": { + "off": "\u0410\u043a\u0442\u0443\u0430\u043b\u0435\u043d", + "on": "\u041d\u0430\u043b\u0438\u0447\u043d\u0430 \u0435 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f" + }, "vibration": { "off": "\u0427\u0438\u0441\u0442\u043e", "on": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u0430" diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json index 089f72f51d5..17c22571c14 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} est\u00e0 desendollat", "is_not_powered": "{entity_name} no est\u00e0 alimentat", "is_not_present": "{entity_name} no est\u00e0 present", + "is_not_tampered": "{entity_name} no detecta manipulaci\u00f3", "is_not_unsafe": "{entity_name} \u00e9s segur", "is_occupied": "{entity_name} est\u00e0 ocupat", "is_off": "{entity_name} est\u00e0 apagat", @@ -42,6 +43,7 @@ "is_problem": "{entity_name} est\u00e0 detectant un problema", "is_smoke": "{entity_name} est\u00e0 detectant fum", "is_sound": "{entity_name} est\u00e0 detectant so", + "is_tampered": "{entity_name} detecta manipulaci\u00f3", "is_unsafe": "{entity_name} \u00e9s insegur", "is_update": "{entity_name} t\u00e9 una actualitzaci\u00f3 disponible", "is_vibration": "{entity_name} est\u00e0 detectant vibraci\u00f3" @@ -52,6 +54,8 @@ "connected": "{entity_name} est\u00e0 connectat", "gas": "{entity_name} ha comen\u00e7at a detectar gas", "hot": "{entity_name} es torna calent", + "is_not_tampered": "{entity_name} ha deixat de detectar manipulaci\u00f3", + "is_tampered": "{entity_name} ha comen\u00e7at a detectar manipulaci\u00f3", "light": "{entity_name} ha comen\u00e7at a detectar llum", "locked": "{entity_name} est\u00e0 bloquejat", "moist": "{entity_name} es torna humit", @@ -95,8 +99,8 @@ }, "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" }, "battery": { "off": "Normal", @@ -170,6 +174,9 @@ "off": "OK", "on": "Problema" }, + "running": { + "on": "En funcionament" + }, "safety": { "off": "Segur", "on": "No segur" diff --git a/homeassistant/components/binary_sensor/translations/de.json b/homeassistant/components/binary_sensor/translations/de.json index 21d1eff1ebf..f6a124b5a0c 100644 --- a/homeassistant/components/binary_sensor/translations/de.json +++ b/homeassistant/components/binary_sensor/translations/de.json @@ -31,6 +31,8 @@ "is_not_plugged_in": "{entity_name} ist nicht angeschlossen", "is_not_powered": "{entity_name} wird nicht mit Strom versorgt", "is_not_present": "{entity_name} ist nicht vorhanden", + "is_not_running": "{entity_name} wird nicht ausgef\u00fchrt", + "is_not_tampered": "{entity_name} erkennt keine Manipulationen", "is_not_unsafe": "{entity_name} ist sicher", "is_occupied": "{entity_name} ist besch\u00e4ftigt / besetzt", "is_off": "{entity_name} ist ausgeschaltet", @@ -40,8 +42,10 @@ "is_powered": "{entity_name} wird mit Strom versorgt", "is_present": "{entity_name} ist vorhanden", "is_problem": "{entity_name} hat ein Problem festgestellt", + "is_running": "{entity_name} wird ausgef\u00fchrt", "is_smoke": "{entity_name} hat Rauch detektiert", "is_sound": "{entity_name} hat Ger\u00e4usche detektiert", + "is_tampered": "{entity_name} erkennt Manipulationen", "is_unsafe": "{entity_name} ist unsicher", "is_update": "{entity_name} hat ein Update verf\u00fcgbar", "is_vibration": "{entity_name} erkennt Vibrationen." @@ -52,6 +56,8 @@ "connected": "{entity_name} verbunden", "gas": "{entity_name} hat Gas detektiert", "hot": "{entity_name} wurde hei\u00df", + "is_not_tampered": "{entity_name} hat aufgeh\u00f6rt, Manipulationen zu erkennen", + "is_tampered": "{entity_name} hat begonnen, Manipulationen zu erkennen", "light": "{entity_name} hat Licht detektiert", "locked": "{entity_name} gesperrt", "moist": "{entity_name} wurde feucht", @@ -77,6 +83,7 @@ "not_plugged_in": "{entity_name} ist nicht angeschlossen", "not_powered": "{entity_name} nicht mit Strom versorgt", "not_present": "{entity_name} nicht anwesend", + "not_running": "{entity_name} wird nicht mehr ausgef\u00fchrt", "not_unsafe": "{entity_name} wurde sicher", "occupied": "{entity_name} wurde besch\u00e4ftigt / besetzt", "opened": "{entity_name} ge\u00f6ffnet", @@ -84,6 +91,7 @@ "powered": "{entity_name} wird mit Strom versorgt", "present": "{entity_name} anwesend", "problem": "{entity_name} hat ein Problem festgestellt", + "running": "{entity_name} ausgef\u00fchrt", "smoke": "{entity_name} detektiert Rauch", "sound": "{entity_name} detektiert Ger\u00e4usche", "turned_off": "{entity_name} ausgeschaltet", @@ -93,6 +101,19 @@ "vibration": "{entity_name} detektiert Vibrationen" } }, + "device_class": { + "cold": "K\u00e4lte", + "gas": "Gas", + "heat": "W\u00e4rme", + "moisture": "Feuchtigkeit", + "motion": "Bewegung", + "occupancy": "Belegung", + "power": "Energie", + "problem": "Problem", + "smoke": "Rauch", + "sound": "Ton", + "vibration": "Vibration" + }, "state": { "_": { "off": "Aus", @@ -170,6 +191,10 @@ "off": "OK", "on": "Problem" }, + "running": { + "off": "Nicht ausgef\u00fchrt", + "on": "L\u00e4uft" + }, "safety": { "off": "Sicher", "on": "Unsicher" diff --git a/homeassistant/components/binary_sensor/translations/en.json b/homeassistant/components/binary_sensor/translations/en.json index 047820498da..80da967cf8b 100644 --- a/homeassistant/components/binary_sensor/translations/en.json +++ b/homeassistant/components/binary_sensor/translations/en.json @@ -31,6 +31,8 @@ "is_not_plugged_in": "{entity_name} is unplugged", "is_not_powered": "{entity_name} is not powered", "is_not_present": "{entity_name} is not present", + "is_not_running": "{entity_name} is not running", + "is_not_tampered": "{entity_name} is not detecting tampering", "is_not_unsafe": "{entity_name} is safe", "is_occupied": "{entity_name} is occupied", "is_off": "{entity_name} is off", @@ -40,8 +42,10 @@ "is_powered": "{entity_name} is powered", "is_present": "{entity_name} is present", "is_problem": "{entity_name} is detecting problem", + "is_running": "{entity_name} is running", "is_smoke": "{entity_name} is detecting smoke", "is_sound": "{entity_name} is detecting sound", + "is_tampered": "{entity_name} is detecting tampering", "is_unsafe": "{entity_name} is unsafe", "is_update": "{entity_name} has an update available", "is_vibration": "{entity_name} is detecting vibration" @@ -52,6 +56,8 @@ "connected": "{entity_name} connected", "gas": "{entity_name} started detecting gas", "hot": "{entity_name} became hot", + "is_not_tampered": "{entity_name} stopped detecting tampering", + "is_tampered": "{entity_name} started detecting tampering", "light": "{entity_name} started detecting light", "locked": "{entity_name} locked", "moist": "{entity_name} became moist", @@ -77,6 +83,7 @@ "not_plugged_in": "{entity_name} unplugged", "not_powered": "{entity_name} not powered", "not_present": "{entity_name} not present", + "not_running": "{entity_name} is no longer running", "not_unsafe": "{entity_name} became safe", "occupied": "{entity_name} became occupied", "opened": "{entity_name} opened", @@ -84,6 +91,7 @@ "powered": "{entity_name} powered", "present": "{entity_name} present", "problem": "{entity_name} started detecting problem", + "running": "{entity_name} started running", "smoke": "{entity_name} started detecting smoke", "sound": "{entity_name} started detecting sound", "turned_off": "{entity_name} turned off", @@ -93,6 +101,19 @@ "vibration": "{entity_name} started detecting vibration" } }, + "device_class": { + "cold": "cold", + "gas": "gas", + "heat": "heat", + "moisture": "moisture", + "motion": "motion", + "occupancy": "occupancy", + "power": "power", + "problem": "problem", + "smoke": "smoke", + "sound": "sound", + "vibration": "vibration" + }, "state": { "_": { "off": "Off", @@ -170,6 +191,10 @@ "off": "OK", "on": "Problem" }, + "running": { + "off": "Not running", + "on": "Running" + }, "safety": { "off": "Safe", "on": "Unsafe" diff --git a/homeassistant/components/binary_sensor/translations/et.json b/homeassistant/components/binary_sensor/translations/et.json index 2a0172300c9..316d1738e62 100644 --- a/homeassistant/components/binary_sensor/translations/et.json +++ b/homeassistant/components/binary_sensor/translations/et.json @@ -31,6 +31,8 @@ "is_not_plugged_in": "{entity_name} on lahti \u00fchendatud", "is_not_powered": "{entity_name} ei ole voolu all", "is_not_present": "{entity_name} puudub", + "is_not_running": "{entity_name} ei t\u00f6\u00f6ta", + "is_not_tampered": "{entity_name} ei tuvasta omavoli", "is_not_unsafe": "{entity_name} on turvaline", "is_occupied": "{entity_name} on h\u00f5ivatud", "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud", @@ -40,8 +42,10 @@ "is_powered": "{entity_name} on voolu all", "is_present": "{entity_name} on saadaval", "is_problem": "Olemil {entity_name} on probleem", + "is_running": "{entity_name} t\u00f6\u00f6tab", "is_smoke": "{entity_name} tuvastab suitsu", "is_sound": "{entity_name} tuvastab heli", + "is_tampered": "{entity_name} tuvastab omavolilise muutmist", "is_unsafe": "{entity_name} on ebaturvaline", "is_update": "{entity_name} on saadaval uuendus", "is_vibration": "{entity_name} tuvastab vibratsiooni" @@ -52,6 +56,8 @@ "connected": "{entity_name} on \u00fchendatud", "gas": "{entity_name} tuvastas gaasi(leket)", "hot": "{entity_name} muutus kuumaks", + "is_not_tampered": "{entity_name} l\u00f5petas omavolilise muutmise tuvastamise", + "is_tampered": "{entity_name} alustas omavolilise muutmise tuvastamist", "light": "{entity_name} tuvastas valgust", "locked": "{entity_name} on lukus", "moist": "{entity_name} muutus niiskeks", @@ -77,6 +83,7 @@ "not_plugged_in": "{entity_name} \u00fchendati vooluv\u00f5rgust v\u00e4lja", "not_powered": "{entity_name} pole toidet", "not_present": "{entity_name} puudub", + "not_running": "{entity_name} ei t\u00f6\u00f6ta enam", "not_unsafe": "{entity_name} muutus turvaliseks", "occupied": "{entity_name} h\u00f5ivati", "opened": "{entity_name} avanes", @@ -84,6 +91,7 @@ "powered": "{entity_name} l\u00fcltus voolu alla", "present": "{entity_name} on saadaval", "problem": "{entity_name} avastas probleemi", + "running": "{entity_name} alustas t\u00f6\u00f6d", "smoke": "{entity_name} tuvastas suitsu", "sound": "{entity_name} tuvastas heli", "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", @@ -93,6 +101,19 @@ "vibration": "{entity_name} registreeris vibratsiooni" } }, + "device_class": { + "cold": "jahutus", + "gas": "gaas", + "heat": "k\u00fcte", + "moisture": "niiskus", + "motion": "liikumine", + "occupancy": "h\u00f5ivatus", + "power": "v\u00f5imsus", + "problem": "probleem", + "smoke": "suits", + "sound": "heli", + "vibration": "vibratsioon" + }, "state": { "_": { "off": "V\u00e4ljas", @@ -170,6 +191,10 @@ "off": "OK", "on": "Probleem" }, + "running": { + "off": "Ei t\u00f6\u00f6ta", + "on": "T\u00f6\u00f6tab" + }, "safety": { "off": "Ohutu", "on": "Ohtlik" diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index 65501c6e698..f6018cfe08a 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -1,16 +1,96 @@ { "device_automation": { "condition_type": { + "is_bat_low": "\u05e1\u05d5\u05dc\u05dc\u05ea {entity_name} \u05d7\u05dc\u05e9\u05d4", "is_cold": "{entity_name} \u05e7\u05e8", + "is_connected": "{entity_name} \u05de\u05d7\u05d5\u05d1\u05e8", + "is_gas": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05d2\u05d6", + "is_hot": "{entity_name} \u05d7\u05dd", "is_light": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05d0\u05d5\u05e8", + "is_locked": "{entity_name} \u05e0\u05e2\u05d5\u05dc", + "is_moist": "{entity_name} \u05dc\u05d7", + "is_motion": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05ea\u05e0\u05d5\u05e2\u05d4", + "is_moving": "{entity_name} \u05d6\u05d6", + "is_no_gas": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05d2\u05d6", "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" + "is_no_motion": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05ea\u05e0\u05d5\u05e2\u05d4", + "is_no_problem": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05d1\u05e2\u05d9\u05d4", + "is_no_smoke": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05e2\u05e9\u05df", + "is_no_sound": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05e6\u05dc\u05d9\u05dc", + "is_no_update": "{entity_name} \u05de\u05e2\u05d5\u05d3\u05db\u05df", + "is_no_vibration": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05e8\u05d8\u05d8", + "is_not_bat_low": "\u05e1\u05d5\u05dc\u05dc\u05ea {entity_name} \u05ea\u05e7\u05d9\u05e0\u05d4", + "is_not_cold": "{entity_name} \u05dc\u05d0 \u05e7\u05e8", + "is_not_connected": "{entity_name} \u05de\u05e0\u05d5\u05ea\u05e7", + "is_not_hot": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05d7\u05dd", + "is_not_locked": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05e0\u05e2\u05d5\u05dc", + "is_not_moist": "{entity_name} \u05d9\u05d1\u05e9", + "is_not_moving": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05d6\u05d6", + "is_not_occupied": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05ea\u05e4\u05d5\u05e1", + "is_not_open": "{entity_name} \u05e1\u05d2\u05d5\u05e8", + "is_not_plugged_in": "{entity_name} \u05de\u05e0\u05d5\u05ea\u05e7", + "is_not_powered": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e4\u05e2\u05dc", + "is_not_present": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05e7\u05d9\u05d9\u05dd", + "is_not_unsafe": "{entity_name} \u05d1\u05d8\u05d5\u05d7", + "is_occupied": "{entity_name} \u05ea\u05e4\u05d5\u05e1", + "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9", + "is_on": "{entity_name} \u05de\u05d5\u05e4\u05e2\u05dc", + "is_open": "{entity_name} \u05e4\u05ea\u05d5\u05d7", + "is_plugged_in": "{entity_name} \u05de\u05d7\u05d5\u05d1\u05e8", + "is_powered": "{entity_name} \u05de\u05d5\u05e4\u05e2\u05dc", + "is_present": "{entity_name} \u05e0\u05d5\u05db\u05d7", + "is_problem": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05d1\u05e2\u05d9\u05d4", + "is_smoke": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05e2\u05e9\u05df", + "is_sound": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05e6\u05dc\u05d9\u05dc", + "is_unsafe": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05d1\u05d8\u05d5\u05d7", + "is_update": "\u05e2\u05d3\u05db\u05d5\u05df \u05d6\u05de\u05d9\u05df \u05e2\u05d1\u05d5\u05e8 {entity_name}", + "is_vibration": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05e8\u05d8\u05d8" }, "trigger_type": { + "bat_low": "\u05e1\u05d5\u05dc\u05dc\u05ea {entity_name} \u05d7\u05dc\u05e9\u05d4", "cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05e7\u05e8", + "connected": "{entity_name} \u05de\u05d7\u05d5\u05d1\u05e8", + "gas": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05d2\u05d6", + "hot": "{entity_name} \u05e0\u05e2\u05e9\u05d4 \u05d7\u05dd", "light": "{entity_name} \u05d4\u05ea\u05d7\u05d9\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05d0\u05d5\u05e8", + "locked": "{entity_name} \u05e0\u05e2\u05d5\u05dc", + "moist": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05d7", + "motion": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05ea\u05e0\u05d5\u05e2\u05d4", + "moving": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05e0\u05d5\u05e2", + "no_gas": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05d2\u05d6", "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" + "no_motion": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05ea\u05e0\u05d5\u05e2\u05d4", + "no_problem": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05d1\u05e2\u05d9\u05d4", + "no_smoke": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05e2\u05e9\u05df", + "no_sound": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05e6\u05dc\u05d9\u05dc", + "no_update": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05de\u05e2\u05d5\u05d3\u05db\u05df", + "no_vibration": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05e8\u05d8\u05d8", + "not_bat_low": "{entity_name} \u05e1\u05d5\u05dc\u05dc\u05d4 \u05e8\u05d2\u05d9\u05dc\u05d4", + "not_cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05dc\u05d0 \u05e7\u05e8", + "not_connected": "{entity_name} \u05de\u05e0\u05d5\u05ea\u05e7", + "not_hot": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05d0 \u05d7\u05dd", + "not_locked": "{entity_name} \u05dc\u05d0 \u05e0\u05e2\u05d5\u05dc", + "not_moist": "{entity_name} \u05d4\u05ea\u05d9\u05d9\u05d1\u05e9", + "not_moving": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d5\u05d6", + "not_occupied": "{entity_name} \u05dc\u05d0 \u05e0\u05ea\u05e4\u05e1", + "not_opened": "{entity_name} \u05e1\u05d2\u05d5\u05e8", + "not_plugged_in": "{entity_name} \u05de\u05e0\u05d5\u05ea\u05e7", + "not_powered": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e4\u05e2\u05dc", + "not_present": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05e7\u05d9\u05d9\u05dd", + "not_unsafe": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05d1\u05d8\u05d5\u05d7", + "occupied": "{entity_name} \u05e0\u05ea\u05e4\u05e1", + "opened": "{entity_name} \u05e0\u05e4\u05ea\u05d7", + "plugged_in": "{entity_name} \u05de\u05d7\u05d5\u05d1\u05e8", + "powered": "{entity_name} \u05de\u05d5\u05e4\u05e2\u05dc", + "present": "{entity_name} \u05e0\u05d5\u05db\u05d7", + "problem": "{entity_name} \u05d4\u05d7\u05dc\u05d4 \u05dc\u05d6\u05d4\u05d5\u05ea \u05d1\u05e2\u05d9\u05d4", + "smoke": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05e2\u05e9\u05df", + "sound": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05e6\u05dc\u05d9\u05dc", + "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", + "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc", + "unsafe": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05dc\u05d0 \u05d1\u05d8\u05d5\u05d7", + "update": "{entity_name} \u05e7\u05d9\u05d1\u05dc \u05e2\u05d3\u05db\u05d5\u05df \u05d6\u05de\u05d9\u05df", + "vibration": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05e8\u05d8\u05d8" } }, "state": { @@ -79,7 +159,8 @@ "on": "\u05e4\u05ea\u05d5\u05d7" }, "plug": { - "off": "\u05de\u05e0\u05d5\u05ea\u05e7" + "off": "\u05de\u05e0\u05d5\u05ea\u05e7", + "on": "\u05de\u05d7\u05d5\u05d1\u05e8" }, "presence": { "off": "\u05d1\u05d7\u05d5\u05e5", diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index d8befd7ae35..9c95cc67d93 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -31,6 +31,8 @@ "is_not_plugged_in": "{entity_name} nincs csatlakoztatva", "is_not_powered": "{entity_name} nincs fesz\u00fcts\u00e9g alatt", "is_not_present": "{entity_name} nincs jelen", + "is_not_running": "{entity_name} nem fut", + "is_not_tampered": "{entity_name} nem \u00e9szlel manipul\u00e1l\u00e1st", "is_not_unsafe": "{entity_name} biztons\u00e1gos", "is_occupied": "{entity_name} foglalt", "is_off": "{entity_name} ki van kapcsolva", @@ -40,8 +42,10 @@ "is_powered": "{entity_name} fesz\u00fclts\u00e9g alatt van", "is_present": "{entity_name} jelen van", "is_problem": "{entity_name} probl\u00e9m\u00e1t \u00e9szlel", + "is_running": "{entity_name} fut", "is_smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", "is_sound": "{entity_name} hangot \u00e9rz\u00e9kel", + "is_tampered": "{entity_name} manipul\u00e1l\u00e1st \u00e9szlel", "is_unsafe": "{entity_name} nem biztons\u00e1gos", "is_update": "{entity_name} egy friss\u00edt\u00e9s \u00e1ll rendelkez\u00e9sre", "is_vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" @@ -52,6 +56,8 @@ "connected": "{entity_name} csatlakozik", "gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", "hot": "{entity_name} felforr\u00f3sodik", + "is_not_tampered": "{entity_name} nem \u00e9szlelt manipul\u00e1l\u00e1st", + "is_tampered": "{entity_name} manipul\u00e1l\u00e1st \u00e9szlelt", "light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", "locked": "{entity_name} be lett z\u00e1rva", "moist": "{entity_name} nedves lett", @@ -77,6 +83,7 @@ "not_plugged_in": "{entity_name} m\u00e1r nincs csatlakoztatva", "not_powered": "{entity_name} m\u00e1r nincs fesz\u00fcts\u00e9g alatt", "not_present": "{entity_name} m\u00e1r nincs jelen", + "not_running": "{entity_name} m\u00e1r nem fut", "not_unsafe": "{entity_name} biztons\u00e1gos lett", "occupied": "{entity_name} foglalt lett", "opened": "{entity_name} ki lett nyitva", @@ -84,6 +91,7 @@ "powered": "{entity_name} m\u00e1r fesz\u00fclts\u00e9g alatt van", "present": "{entity_name} m\u00e1r jelen van", "problem": "{entity_name} probl\u00e9m\u00e1t \u00e9szlel", + "running": "{entity_name} elindult", "smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", "sound": "{entity_name} hangot \u00e9rz\u00e9kel", "turned_off": "{entity_name} ki lett kapcsolva", @@ -170,6 +178,10 @@ "off": "OK", "on": "Probl\u00e9ma" }, + "running": { + "off": "Nem fut", + "on": "Fut" + }, "safety": { "off": "Biztons\u00e1gos", "on": "Nem biztons\u00e1gos" diff --git a/homeassistant/components/binary_sensor/translations/is.json b/homeassistant/components/binary_sensor/translations/is.json index bd1ed9c389a..46b44913169 100644 --- a/homeassistant/components/binary_sensor/translations/is.json +++ b/homeassistant/components/binary_sensor/translations/is.json @@ -48,6 +48,10 @@ "off": "Engin vi\u00f0vera", "on": "Uppg\u00f6tva\u00f0" }, + "opening": { + "off": "Loka\u00f0", + "on": "Opi\u00f0" + }, "presence": { "off": "Fjarverandi", "on": "Heima" diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index b6301ed8f62..ef16af64af7 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} \u00e8 collegato", "is_not_powered": "{entity_name} non \u00e8 alimentato", "is_not_present": "{entity_name} non \u00e8 presente", + "is_not_tampered": "{entity_name} non rileva manomissioni", "is_not_unsafe": "{entity_name} \u00e8 sicuro", "is_occupied": "{entity_name} \u00e8 occupato", "is_off": "{entity_name} \u00e8 spento", @@ -42,6 +43,7 @@ "is_problem": "{entity_name} sta rilevando un problema", "is_smoke": "{entity_name} sta rilevando il fumo", "is_sound": "{entity_name} sta rilevando il suono", + "is_tampered": "{entity_name} rileva manomissioni", "is_unsafe": "{entity_name} non \u00e8 sicuro", "is_update": "{entity_name} ha un aggiornamento disponibile", "is_vibration": "{entity_name} sta rilevando la vibrazione" @@ -52,6 +54,8 @@ "connected": "{entity_name} connesso", "gas": "{entity_name} ha iniziato a rilevare il gas", "hot": "{entity_name} \u00e8 diventato caldo", + "is_not_tampered": "{entity_name} ha smesso di rilevare manomissioni", + "is_tampered": "{entity_name} ha iniziato a rilevare manomissioni", "light": "{entity_name} ha iniziato a rilevare la luce", "locked": "{entity_name} bloccato", "moist": "{entity_name} diventato umido", diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 5434f8687bf..54280a5334a 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -17,12 +17,12 @@ "on": "\u63a5\u7d9a\u6e08" }, "door": { - "off": "\u9589\u9396", - "on": "\u958b\u653e" + "off": "\u9589", + "on": "\u958b" }, "garage_door": { - "off": "\u9589\u9396", - "on": "\u958b\u653e" + "off": "\u9589", + "on": "\u958b" }, "gas": { "off": "\u672a\u691c\u51fa", diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index f395335c627..1abf0b86bca 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -52,6 +52,7 @@ "connected": "{entity_name} verbonden", "gas": "{entity_name} begon gas te detecteren", "hot": "{entity_name} werd heet", + "is_tampered": "{entity_name} begonnen met het detecteren van sabotage", "light": "{entity_name} begon licht te detecteren", "locked": "{entity_name} vergrendeld", "moist": "{entity_name} werd vochtig", diff --git a/homeassistant/components/binary_sensor/translations/no.json b/homeassistant/components/binary_sensor/translations/no.json index 041643f9cc3..7dd6243edf8 100644 --- a/homeassistant/components/binary_sensor/translations/no.json +++ b/homeassistant/components/binary_sensor/translations/no.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} er koblet fra", "is_not_powered": "{entity_name} er spenningsl\u00f8s", "is_not_present": "{entity_name} er ikke tilstede", + "is_not_tampered": "{entity_name} oppdager ikke manipulering", "is_not_unsafe": "{entity_name} er trygg", "is_occupied": "{entity_name} er opptatt", "is_off": "{entity_name} er sl\u00e5tt av", @@ -42,6 +43,7 @@ "is_problem": "{entity_name} registrerer et problem", "is_smoke": "{entity_name} registrerer r\u00f8yk", "is_sound": "{entity_name} registrerer lyd", + "is_tampered": "{entity_name} oppdager manipulering", "is_unsafe": "{entity_name} er utrygg", "is_update": "{entity_name} har en tilgjengelig oppdatering", "is_vibration": "{entity_name} registrerer vibrasjon" @@ -52,6 +54,8 @@ "connected": "{entity_name} tilkoblet", "gas": "{entity_name} begynte \u00e5 registrere gass", "hot": "{entity_name} ble varm", + "is_not_tampered": "{entity_name} sluttet \u00e5 oppdage manipulering", + "is_tampered": "{entity_name} begynte \u00e5 oppdage manipulering", "light": "{entity_name} begynte \u00e5 registrere lys", "locked": "{entity_name} l\u00e5st", "moist": "{entity_name} ble fuktig", diff --git a/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant/components/binary_sensor/translations/pl.json index 7b89d566a63..648b48a178f 100644 --- a/homeassistant/components/binary_sensor/translations/pl.json +++ b/homeassistant/components/binary_sensor/translations/pl.json @@ -31,6 +31,8 @@ "is_not_plugged_in": "sensor {entity_name} wykrywa od\u0142\u0105czenie", "is_not_powered": "sensor {entity_name} nie wykrywa zasilania", "is_not_present": "sensor {entity_name} nie wykrywa obecno\u015bci", + "is_not_running": "{entity_name} nie dzia\u0142a", + "is_not_tampered": "sensor {entity_name} nie wykrywa naruszenia", "is_not_unsafe": "sensor {entity_name} nie wykrywa zagro\u017cenia", "is_occupied": "sensor {entity_name} jest zaj\u0119ty", "is_off": "sensor {entity_name} jest wy\u0142\u0105czony", @@ -40,8 +42,10 @@ "is_powered": "sensor {entity_name} wykrywa zasilanie", "is_present": "sensor {entity_name} wykrywa obecno\u015b\u0107", "is_problem": "sensor {entity_name} wykrywa problem", + "is_running": "{entity_name} dzia\u0142a", "is_smoke": "sensor {entity_name} wykrywa dym", "is_sound": "sensor {entity_name} wykrywa d\u017awi\u0119k", + "is_tampered": "sensor {entity_name} wykrywa naruszenie", "is_unsafe": "sensor {entity_name} wykrywa zagro\u017cenie", "is_update": "dla {entity_name} jest dost\u0119pna aktualizacja", "is_vibration": "sensor {entity_name} wykrywa wibracje" @@ -52,6 +56,8 @@ "connected": "nast\u0105pi pod\u0142\u0105czenie {entity_name}", "gas": "sensor {entity_name} wykryje gaz", "hot": "sensor {entity_name} wykryje gor\u0105co", + "is_not_tampered": "sensor {entity_name} przestanie wykrywa\u0107 naruszenie", + "is_tampered": "sensor {entity_name} wykryje naruszenie", "light": "sensor {entity_name} wykryje \u015bwiat\u0142o", "locked": "nast\u0105pi zamkni\u0119cie {entity_name}", "moist": "nast\u0105pi wykrycie wilgoci {entity_name}", @@ -77,6 +83,7 @@ "not_plugged_in": "nast\u0105pi od\u0142\u0105czenie {entity_name}", "not_powered": "nast\u0105pi od\u0142\u0105czenie zasilania {entity_name}", "not_present": "sensor {entity_name} przestanie wykrywa\u0107 obecno\u015b\u0107", + "not_running": "zako\u0144czy si\u0119 dzia\u0142anie {entity_name}", "not_unsafe": "sensor {entity_name} przestanie wykrywa\u0107 zagro\u017cenie", "occupied": "sensor {entity_name} stanie si\u0119 zaj\u0119ty", "opened": "nast\u0105pi otwarcie {entity_name}", @@ -84,6 +91,7 @@ "powered": "nast\u0105pi pod\u0142\u0105czenie zasilenia {entity_name}", "present": "sensor {entity_name} wykryje obecno\u015b\u0107", "problem": "sensor {entity_name} wykryje problem", + "running": "rozpocznie si\u0119 dzia\u0142anie {entity_name}", "smoke": "sensor {entity_name} wykryje dym", "sound": "sensor {entity_name} wykryje d\u017awi\u0119k", "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", @@ -170,6 +178,10 @@ "off": "ok", "on": "problem" }, + "running": { + "off": "nie dzia\u0142a", + "on": "dzia\u0142a" + }, "safety": { "off": "brak zagro\u017cenia", "on": "zagro\u017cenie" diff --git a/homeassistant/components/binary_sensor/translations/pt-BR.json b/homeassistant/components/binary_sensor/translations/pt-BR.json index 52671ca0425..385d8620d76 100644 --- a/homeassistant/components/binary_sensor/translations/pt-BR.json +++ b/homeassistant/components/binary_sensor/translations/pt-BR.json @@ -1,4 +1,14 @@ { + "device_automation": { + "condition_type": { + "is_motion": "{entity_name} est\u00e1 detectando movimento", + "is_no_motion": "{entity_name} n\u00e3o est\u00e1 detectando movimento" + }, + "trigger_type": { + "motion": "{entity_name} come\u00e7ou a detectar movimento", + "no_motion": "{entity_name} parou de detectar movimento" + } + }, "state": { "_": { "off": "Desligado", diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json index c245d2ba15a..09a9da61e20 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -31,6 +31,8 @@ "is_not_plugged_in": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", "is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0438\u0442\u0430\u043d\u0438\u0435", "is_not_present": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "is_not_running": "{entity_name} \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442", + "is_not_tampered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0435", "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_occupied": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", @@ -40,8 +42,10 @@ "is_powered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0438\u0442\u0430\u043d\u0438\u0435", "is_present": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "is_running": "{entity_name} \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442", "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", "is_sound": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", + "is_tampered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0435", "is_unsafe": "{entity_name} \u0432 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_update": "\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 {entity_name}", "is_vibration": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" @@ -52,6 +56,8 @@ "connected": "{entity_name} \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "gas": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437", "hot": "{entity_name} \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0435\u0442\u0441\u044f", + "is_not_tampered": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0435", + "is_tampered": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0435", "light": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442", "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", "moist": "{entity_name} \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0432\u043b\u0430\u0436\u043d\u044b\u043c", @@ -77,6 +83,7 @@ "not_plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", "not_present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "not_running": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c", "not_unsafe": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", "occupied": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "opened": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", @@ -84,6 +91,7 @@ "powered": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", "present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "problem": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "running": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c", "smoke": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", "sound": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a", "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", @@ -170,6 +178,10 @@ "off": "\u041e\u041a", "on": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430" }, + "running": { + "off": "\u041d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442", + "on": "\u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442" + }, "safety": { "off": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e", "on": "\u041d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e" diff --git a/homeassistant/components/binary_sensor/translations/zh-Hans.json b/homeassistant/components/binary_sensor/translations/zh-Hans.json index 82cd0d3ccfe..0c556e7a9c0 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hans.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hans.json @@ -178,6 +178,10 @@ "off": "\u6b63\u5e38", "on": "\u89e6\u53d1" }, + "update": { + "off": "\u5df2\u662f\u6700\u65b0", + "on": "\u6709\u66f4\u65b0" + }, "vibration": { "off": "\u6b63\u5e38", "on": "\u89e6\u53d1" diff --git a/homeassistant/components/binary_sensor/translations/zh-Hant.json b/homeassistant/components/binary_sensor/translations/zh-Hant.json index 4733d4d1dcc..5f27ce7319a 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hant.json @@ -31,6 +31,8 @@ "is_not_plugged_in": "{entity_name}\u672a\u63d2\u5165", "is_not_powered": "{entity_name}\u672a\u901a\u96fb", "is_not_present": "{entity_name}\u672a\u51fa\u73fe", + "is_not_running": "{entity_name} \u672a\u5728\u57f7\u884c", + "is_not_tampered": "{entity_name}\u672a\u5075\u6e2c\u5230\u6e1b\u5f31", "is_not_unsafe": "{entity_name}\u5b89\u5168", "is_occupied": "{entity_name}\u6709\u4eba", "is_off": "{entity_name}\u95dc\u9589", @@ -40,8 +42,10 @@ "is_powered": "{entity_name}\u901a\u96fb", "is_present": "{entity_name}\u51fa\u73fe", "is_problem": "{entity_name}\u6b63\u5075\u6e2c\u5230\u554f\u984c", + "is_running": "{entity_name} \u6b63\u5728\u57f7\u884c", "is_smoke": "{entity_name}\u6b63\u5075\u6e2c\u5230\u7159\u9727", "is_sound": "{entity_name}\u6b63\u5075\u6e2c\u5230\u8072\u97f3", + "is_tampered": "{entity_name}\u5075\u6e2c\u5230\u6e1b\u5f31\u4f5c\u4e2d", "is_unsafe": "{entity_name}\u4e0d\u5b89\u5168", "is_update": "{entity_name} \u6709\u66f4\u65b0", "is_vibration": "{entity_name}\u6b63\u5075\u6e2c\u5230\u9707\u52d5" @@ -52,6 +56,8 @@ "connected": "{entity_name}\u5df2\u9023\u7dda", "gas": "{entity_name}\u5df2\u958b\u59cb\u5075\u6e2c\u6c23\u9ad4", "hot": "{entity_name}\u5df2\u8b8a\u71b1", + "is_not_tampered": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u6e1b\u5f31", + "is_tampered": "{entity_name}\u5df2\u5075\u6e2c\u5230\u6e1b\u5f31", "light": "{entity_name}\u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda", "locked": "{entity_name}\u5df2\u4e0a\u9396", "moist": "{entity_name}\u5df2\u8b8a\u6f6e\u6fd5", @@ -77,6 +83,7 @@ "not_plugged_in": "{entity_name}\u672a\u63d2\u5165", "not_powered": "{entity_name}\u672a\u901a\u96fb", "not_present": "{entity_name}\u672a\u51fa\u73fe", + "not_running": "{entity_name} \u4e0d\u518d\u57f7\u884c", "not_unsafe": "{entity_name}\u5df2\u5b89\u5168", "occupied": "{entity_name}\u8b8a\u6210\u6709\u4eba", "opened": "{entity_name}\u5df2\u958b\u555f", @@ -84,6 +91,7 @@ "powered": "{entity_name}\u5df2\u901a\u96fb", "present": "{entity_name}\u5df2\u51fa\u73fe", "problem": "{entity_name}\u5df2\u5075\u6e2c\u5230\u554f\u984c", + "running": "{entity_name} \u958b\u59cb\u57f7\u884c", "smoke": "{entity_name}\u5df2\u5075\u6e2c\u5230\u7159\u9727", "sound": "{entity_name}\u5df2\u5075\u6e2c\u5230\u8072\u97f3", "turned_off": "{entity_name}\u5df2\u95dc\u9589", @@ -170,6 +178,10 @@ "off": "\u78ba\u5b9a", "on": "\u7570\u5e38" }, + "running": { + "off": "\u672a\u57f7\u884c", + "on": "\u57f7\u884c\u4e2d" + }, "safety": { "off": "\u5b89\u5168", "on": "\u5371\u96aa" @@ -195,5 +207,5 @@ "on": "\u958b\u555f" } }, - "title": "\u4e8c\u9032\u4f4d\u50b3\u611f\u5668" + "title": "\u4e8c\u9032\u4f4d\u611f\u6e2c\u5668" } \ No newline at end of file diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index b66f775eae2..553d0aafa05 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_OPTIONS, TIME_MINUTES, @@ -165,7 +164,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BitcoinSensor(SensorEntity): """Representation of a Bitcoin sensor.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION _attr_icon = ICON def __init__(self, data, currency, description: SensorEntityDescription): diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index 95b36612add..681fff4a9bc 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -82,13 +82,13 @@ class BleBoxEntity(Entity): self._attr_name = feature.full_name self._attr_unique_id = feature.unique_id product = feature.product - self._attr_device_info = { - "identifiers": {(DOMAIN, product.unique_id)}, - "name": product.name, - "manufacturer": product.brand, - "model": product.model, - "sw_version": product.firmware_version, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, product.unique_id)}, + manufacturer=product.brand, + model=product.model, + name=product.name, + sw_version=product.firmware_version, + ) async def async_update(self): """Update the entity state.""" diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index 59e64b772ef..5dec5725607 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -40,8 +40,7 @@ class BleBoxClimateEntity(BleBoxEntity, ClimateEntity): @property def hvac_action(self): """Return the actual current HVAC action.""" - is_on = self._feature.is_on - if not is_on: + if not (is_on := self._feature.is_on): return None if is_on is None else CURRENT_HVAC_OFF # NOTE: In practice, there's no need to handle case when is_heating is None diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 5dc6a486ed3..b107dba1e7f 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -35,11 +35,6 @@ class BleBoxCoverEntity(BleBoxEntity, CoverEntity): stop = SUPPORT_STOP if feature.has_stop else 0 self._attr_supported_features = position | stop | SUPPORT_OPEN | SUPPORT_CLOSE - @property - def state(self): - """Return the equivalent HA cover state.""" - return BLEBOX_TO_HASS_COVER_STATES[self._feature.state] - @property def current_cover_position(self): """Return the current cover position.""" @@ -83,5 +78,5 @@ class BleBoxCoverEntity(BleBoxEntity, CoverEntity): await self._feature.async_stop() def _is_state(self, state_name): - value = self.state + value = BLEBOX_TO_HASS_COVER_STATES[self._feature.state] return None if value is None else value == state_name diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index b03cc16112c..efbdb038794 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -56,8 +56,7 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): @property def rgbw_color(self): """Return the hue and saturation.""" - rgbw_hex = self._feature.rgbw_hex - if rgbw_hex is None: + if (rgbw_hex := self._feature.rgbw_hex) is None: return None return tuple(rgb_hex_to_rgb_list(rgbw_hex)[0:4]) diff --git a/homeassistant/components/blebox/translations/bg.json b/homeassistant/components/blebox/translations/bg.json new file mode 100644 index 00000000000..11108007b21 --- /dev/null +++ b/homeassistant/components/blebox/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/hu.json b/homeassistant/components/blebox/translations/hu.json index 056402ea13f..0d9e2b5a3ff 100644 --- a/homeassistant/components/blebox/translations/hu.json +++ b/homeassistant/components/blebox/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "address_already_configured": "Egy BleBox-eszk\u00f6z m\u00e1r konfigur\u00e1lva van a(z) {address} c\u00edmen.", + "address_already_configured": "Egy BleBox-eszk\u00f6z m\u00e1r konfigur\u00e1lva van a {address} c\u00edmen.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { diff --git a/homeassistant/components/blebox/translations/tr.json b/homeassistant/components/blebox/translations/tr.json index 31df3fb5e30..6acd2cf7d43 100644 --- a/homeassistant/components/blebox/translations/tr.json +++ b/homeassistant/components/blebox/translations/tr.json @@ -6,7 +6,8 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "unknown": "Beklenmeyen hata" + "unknown": "Beklenmeyen hata", + "unsupported_version": "BleBox cihaz\u0131n\u0131n g\u00fcncel olmayan bellenimi var. L\u00fctfen \u00f6nce y\u00fckseltin." }, "step": { "user": { diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 8b4f1ba4eec..9a617bbc781 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -46,16 +46,18 @@ class BlinkCamera(Camera): def enable_motion_detection(self): """Enable motion detection for the camera.""" - self._camera.set_motion_detect(True) + self._camera.arm = True + self.data.refresh() def disable_motion_detection(self): """Disable motion detection for the camera.""" - self._camera.set_motion_detect(False) + self._camera.arm = False + self.data.refresh() @property def motion_detection_enabled(self): """Return the state of the camera.""" - return self._camera.motion_enabled + return self._camera.arm @property def brand(self): diff --git a/homeassistant/components/blink/translations/bg.json b/homeassistant/components/blink/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/blink/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/tr.json b/homeassistant/components/blink/translations/tr.json index 8193ff9d8be..cef806cb309 100644 --- a/homeassistant/components/blink/translations/tr.json +++ b/homeassistant/components/blink/translations/tr.json @@ -11,6 +11,9 @@ }, "step": { "2fa": { + "data": { + "2fa": "\u0130ki ad\u0131ml\u0131 kimlik do\u011frulama kodu" + }, "description": "E-postan\u0131za g\u00f6nderilen PIN kodunu girin" }, "user": { diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index fa8d3160dc8..e04f4731918 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -1,17 +1,13 @@ """Support for BloomSky weather station.""" from datetime import timedelta +from http import HTTPStatus import logging from aiohttp.hdrs import AUTHORIZATION import requests import voluptuous as vol -from homeassistant.const import ( - CONF_API_KEY, - HTTP_METHOD_NOT_ALLOWED, - HTTP_OK, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_API_KEY from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -72,12 +68,12 @@ class BloomSky: headers={AUTHORIZATION: self._api_key}, timeout=10, ) - if response.status_code == HTTP_UNAUTHORIZED: + if response.status_code == HTTPStatus.UNAUTHORIZED: raise RuntimeError("Invalid API_KEY") - if response.status_code == HTTP_METHOD_NOT_ALLOWED: + if response.status_code == HTTPStatus.METHOD_NOT_ALLOWED: _LOGGER.error("You have no bloomsky devices configured") return - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: _LOGGER.error("Invalid HTTP response: %s", response.status_code) return # Create dictionary keyed off of the device unique id diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 827d37843d9..a8146764710 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -255,8 +255,7 @@ class DomainBlueprints: def load_from_cache(): """Load blueprint from cache.""" - blueprint = self._blueprints[blueprint_path] - if blueprint is None: + if (blueprint := self._blueprints[blueprint_path]) is None: raise FailedToLoad( self.domain, blueprint_path, diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 86d0be72bdc..6c90a511a05 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -2,6 +2,7 @@ import asyncio from asyncio import CancelledError from datetime import timedelta +from http import HTTPStatus import logging from urllib import parse @@ -38,7 +39,6 @@ from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - HTTP_OK, STATE_IDLE, STATE_OFF, STATE_PAUSED, @@ -106,8 +106,6 @@ SERVICE_TO_METHOD = { def _add_player(hass, async_add_entities, host, port=None, name=None): """Add Bluesound players.""" - if host in [x.host for x in hass.data[DATA_BLUESOUND]]: - return @callback def _init_player(event=None): @@ -127,6 +125,11 @@ def _add_player(hass, async_add_entities, host, port=None, name=None): @callback def _add_player_cb(): """Add player after first sync fetch.""" + if player.id in [x.id for x in hass.data[DATA_BLUESOUND]]: + _LOGGER.warning("Player already added %s", player.id) + return + + hass.data[DATA_BLUESOUND].append(player) async_add_entities([player]) _LOGGER.info("Added device with name: %s", player.name) @@ -138,7 +141,6 @@ def _add_player(hass, async_add_entities, host, port=None, name=None): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_polling) player = BluesoundPlayer(hass, host, port, name, _add_player_cb) - hass.data[DATA_BLUESOUND].append(player) if hass.is_running: _init_player() @@ -160,8 +162,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) return - hosts = config.get(CONF_HOSTS) - if hosts: + if hosts := config.get(CONF_HOSTS): for host in hosts: _add_player( hass, @@ -173,15 +174,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_service_handler(service): """Map services to method of Bluesound devices.""" - method = SERVICE_TO_METHOD.get(service.service) - if not method: + if not (method := SERVICE_TO_METHOD.get(service.service)): return params = { key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID } - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: + if entity_ids := service.data.get(ATTR_ENTITY_ID): target_players = [ player for player in hass.data[DATA_BLUESOUND] @@ -211,6 +210,7 @@ class BluesoundPlayer(MediaPlayerEntity): self._polling_session = async_get_clientsession(hass) self._polling_task = None # The actual polling task. self._name = name + self._id = None self._icon = None self._capture_items = [] self._services_items = [] @@ -228,6 +228,7 @@ class BluesoundPlayer(MediaPlayerEntity): self._bluesound_device_name = None self._init_callback = init_callback + if self.port is None: self.port = DEFAULT_PORT @@ -254,26 +255,29 @@ class BluesoundPlayer(MediaPlayerEntity): if not self._name: self._name = self._sync_status.get("@name", self.host) + if not self._id: + self._id = self._sync_status.get("@id", None) if not self._bluesound_device_name: self._bluesound_device_name = self._sync_status.get("@name", self.host) if not self._icon: self._icon = self._sync_status.get("@icon", self.host) - master = self._sync_status.get("master") - if master is not None: + if (master := self._sync_status.get("master")) is not None: self._is_master = False master_host = master.get("#text") + master_port = master.get("@port", "11000") + master_id = f"{master_host}:{master_port}" master_device = [ device for device in self._hass.data[DATA_BLUESOUND] - if device.host == master_host + if device.id == master_id ] - if master_device and master_host != self.host: + if master_device and master_id != self.id: self._master = master_device[0] else: self._master = None - _LOGGER.error("Master not found %s", master_host) + _LOGGER.error("Master not found %s", master_id) else: if self._master is not None: self._master = None @@ -291,14 +295,14 @@ class BluesoundPlayer(MediaPlayerEntity): await self.async_update_status() except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException): - _LOGGER.info("Node %s is offline, retrying later", self._name) + _LOGGER.info("Node %s:%s is offline, retrying later", self.name, self.port) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) self.start_polling() except CancelledError: - _LOGGER.debug("Stopping the polling of node %s", self._name) + _LOGGER.debug("Stopping the polling of node %s:%s", self.name, self.port) except Exception: - _LOGGER.exception("Unexpected error in %s", self._name) + _LOGGER.exception("Unexpected error in %s:%s", self.name, self.port) raise def start_polling(self): @@ -318,12 +322,14 @@ class BluesoundPlayer(MediaPlayerEntity): await self.force_update_sync_status(self._init_callback, True) except (asyncio.TimeoutError, ClientError): - _LOGGER.info("Node %s is offline, retrying later", self.host) + _LOGGER.info("Node %s:%s is offline, retrying later", self.host, self.port) self._retry_remove = async_track_time_interval( self._hass, self.async_init, NODE_RETRY_INITIATION ) except Exception: - _LOGGER.exception("Unexpected when initiating error in %s", self.host) + _LOGGER.exception( + "Unexpected when initiating error in %s:%s", self.host, self.port + ) raise async def async_update(self): @@ -355,7 +361,7 @@ class BluesoundPlayer(MediaPlayerEntity): with async_timeout.timeout(10): response = await websession.get(url) - if response.status == HTTP_OK: + if response.status == HTTPStatus.OK: result = await response.text() if result: data = xmltodict.parse(result) @@ -370,9 +376,9 @@ class BluesoundPlayer(MediaPlayerEntity): except (asyncio.TimeoutError, aiohttp.ClientError): if raise_timeout: - _LOGGER.info("Timeout: %s", self.host) + _LOGGER.info("Timeout: %s:%s", self.host, self.port) raise - _LOGGER.debug("Failed communicating: %s", self.host) + _LOGGER.debug("Failed communicating: %s:%s", self.host, self.port) return None return data @@ -399,7 +405,7 @@ class BluesoundPlayer(MediaPlayerEntity): url, headers={CONNECTION: KEEP_ALIVE} ) - if response.status == HTTP_OK: + if response.status == HTTPStatus.OK: result = await response.text() self._is_online = True self._last_status_update = dt_util.utcnow() @@ -407,7 +413,7 @@ class BluesoundPlayer(MediaPlayerEntity): group_name = self._status.get("groupName") if group_name != self._group_name: - _LOGGER.debug("Group name change detected on device: %s", self.host) + _LOGGER.debug("Group name change detected on device: %s", self.id) self._group_name = group_name # rebuild ordered list of entity_ids that are in the group, master is first @@ -580,8 +586,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self.is_grouped and not self.is_master: return self._group_name - artist = self._status.get("artist") - if not artist: + if not (artist := self._status.get("artist")): artist = self._status.get("title2") return artist @@ -591,8 +596,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._status is None or (self.is_grouped and not self.is_master): return None - album = self._status.get("album") - if not album: + if not (album := self._status.get("album")): album = self._status.get("title3") return album @@ -602,8 +606,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._status is None or (self.is_grouped and not self.is_master): return None - url = self._status.get("image") - if not url: + if not (url := self._status.get("image")): return if url[0] == "/": url = f"http://{self.host}:{self.port}{url}" @@ -620,8 +623,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._last_status_update is None or mediastate == STATE_IDLE: return None - position = self._status.get("secs") - if position is None: + if (position := self._status.get("secs")) is None: return None position = float(position) @@ -636,8 +638,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._status is None or (self.is_grouped and not self.is_master): return None - duration = self._status.get("totlen") - if duration is None: + if (duration := self._status.get("totlen")) is None: return None return float(duration) @@ -668,6 +669,11 @@ class BluesoundPlayer(MediaPlayerEntity): mute = bool(int(mute)) return mute + @property + def id(self): + """Get id of device.""" + return self._id + @property def name(self): """Return the name of the device.""" @@ -712,8 +718,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._status is None or (self.is_grouped and not self.is_master): return None - current_service = self._status.get("service", "") - if current_service == "": + if (current_service := self._status.get("service", "")) == "": return "" stream_url = self._status.get("streamUrl", "") @@ -841,8 +846,8 @@ class BluesoundPlayer(MediaPlayerEntity): if master_device: _LOGGER.debug( "Trying to join player: %s to master: %s", - self.host, - master_device[0].host, + self.id, + master_device[0].id, ) await master_device[0].async_add_slave(self) @@ -887,7 +892,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._master is None: return - _LOGGER.debug("Trying to unjoin player: %s", self.host) + _LOGGER.debug("Trying to unjoin player: %s", self.id) await self._master.async_remove_slave(self) async def async_add_slave(self, slave_device): @@ -906,7 +911,7 @@ class BluesoundPlayer(MediaPlayerEntity): """Increase sleep time on player.""" sleep_time = await self.send_bluesound_command("/Sleep") if sleep_time is None: - _LOGGER.error("Error while increasing sleep time on player: %s", self.host) + _LOGGER.error("Error while increasing sleep time on player: %s", self.id) return 0 return int(sleep_time.get("sleep", "0")) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 85a5c9cd02f..c7592c5db34 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,27 +1,29 @@ """Reads vehicle status from BMW connected drive portal.""" from __future__ import annotations +from collections.abc import Callable import logging +from typing import Any, cast from bimmer_connected.account import ConnectedDriveAccount from bimmer_connected.country_selector import get_region_from_name +from bimmer_connected.vehicle import ConnectedDriveVehicle import voluptuous as vol from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_DEVICE_ID, CONF_NAME, CONF_PASSWORD, CONF_REGION, CONF_USERNAME, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry, discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify @@ -74,6 +76,7 @@ _SERVICE_MAP = { "light_flash": "trigger_remote_light_flash", "sound_horn": "trigger_remote_horn", "activate_air_conditioning": "trigger_remote_air_conditioning", + "deactivate_air_conditioning": "trigger_remote_air_conditioning_stop", "find_vehicle": "trigger_remote_vehicle_finder", } @@ -97,7 +100,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback -def _async_migrate_options_from_data_if_missing(hass, entry): +def _async_migrate_options_from_data_if_missing( + hass: HomeAssistant, entry: ConfigEntry +) -> None: data = dict(entry.data) options = dict(entry.options) @@ -122,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except OSError as ex: raise ConfigEntryNotReady from ex - async def _async_update_all(service_call=None): + async def _async_update_all(service_call: ServiceCall | None = None) -> None: """Update all BMW accounts.""" await hass.async_add_executor_job(_update_all) @@ -163,7 +168,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( entry, [platform for platform in PLATFORMS if platform != NOTIFY_DOMAIN] @@ -190,18 +195,20 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def update_listener(hass, config_entry): +async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(config_entry.entry_id) -def setup_account(entry: ConfigEntry, hass, name: str) -> BMWConnectedDriveAccount: +def setup_account( + entry: ConfigEntry, hass: HomeAssistant, name: str +) -> BMWConnectedDriveAccount: """Set up a new BMWConnectedDriveAccount based on the config.""" - username = entry.data[CONF_USERNAME] - password = entry.data[CONF_PASSWORD] - region = entry.data[CONF_REGION] - read_only = entry.options[CONF_READ_ONLY] - use_location = entry.options[CONF_USE_LOCATION] + username: str = entry.data[CONF_USERNAME] + password: str = entry.data[CONF_PASSWORD] + region: str = entry.data[CONF_REGION] + read_only: bool = entry.options[CONF_READ_ONLY] + use_location: bool = entry.options[CONF_USE_LOCATION] _LOGGER.debug("Adding new account %s", name) @@ -212,16 +219,21 @@ def setup_account(entry: ConfigEntry, hass, name: str) -> BMWConnectedDriveAccou username, password, region, name, read_only, *pos ) - def execute_service(call): + def execute_service(call: ServiceCall) -> None: """Execute a service for a vehicle.""" - vin = call.data.get(ATTR_VIN) - device_id = call.data.get(CONF_DEVICE_ID) + vin: str | None = call.data.get(ATTR_VIN) + device_id: str | None = call.data.get(CONF_DEVICE_ID) - vehicle = None + vehicle: ConnectedDriveVehicle | None = None if not vin and device_id: - device = device_registry.async_get(hass).async_get(device_id) + # If vin is None, device_id must be set (given by SERVICE_SCHEMA) + if not (device := device_registry.async_get(hass).async_get(device_id)): + _LOGGER.error("Could not find a device for id: %s", device_id) + return vin = next(iter(device.identifiers))[1] + else: + vin = cast(str, vin) # Double check for read_only accounts as another account could create the services for entry_data in [ @@ -229,8 +241,8 @@ def setup_account(entry: ConfigEntry, hass, name: str) -> BMWConnectedDriveAccou for e in hass.data[DOMAIN][DATA_ENTRIES].values() if not e[CONF_ACCOUNT].read_only ]: - vehicle = entry_data[CONF_ACCOUNT].account.get_vehicle(vin) - if vehicle: + account: ConnectedDriveAccount = entry_data[CONF_ACCOUNT].account + if vehicle := account.get_vehicle(vin): break if not vehicle: _LOGGER.error("Could not find a vehicle for VIN %s", vin) @@ -272,8 +284,8 @@ class BMWConnectedDriveAccount: region_str: str, name: str, read_only: bool, - lat=None, - lon=None, + lat: float | None = None, + lon: float | None = None, ) -> None: """Initialize account.""" region = get_region_from_name(region_str) @@ -281,7 +293,7 @@ class BMWConnectedDriveAccount: self.read_only = read_only self.account = ConnectedDriveAccount(username, password, region) self.name = name - self._update_listeners = [] + self._update_listeners: list[Callable[[], None]] = [] # Set observer position once for older cars to be in range for # GPS position (pre-7/2014, <2km) and get new data from API @@ -289,7 +301,7 @@ class BMWConnectedDriveAccount: self.account.set_observer_position(lat, lon) self.account.update_vehicle_states() - def update(self, *_): + def update(self, *_: Any) -> None: """Update the state of all vehicles. Notify all listeners about the update. @@ -310,7 +322,7 @@ class BMWConnectedDriveAccount: ) _LOGGER.exception(exception) - def add_update_listener(self, listener): + def add_update_listener(self, listener: Callable[[], None]) -> None: """Add a listener for update notifications.""" self._update_listeners.append(listener) @@ -319,28 +331,32 @@ class BMWConnectedDriveBaseEntity(Entity): """Common base for BMW entities.""" _attr_should_poll = False + _attr_attribution = ATTRIBUTION - def __init__(self, account, vehicle): + def __init__( + self, + account: BMWConnectedDriveAccount, + vehicle: ConnectedDriveVehicle, + ) -> None: """Initialize sensor.""" self._account = account self._vehicle = vehicle - self._attrs = { + self._attrs: dict[str, Any] = { "car": self._vehicle.name, "vin": self._vehicle.vin, - ATTR_ATTRIBUTION: ATTRIBUTION, - } - self._attr_device_info = { - "identifiers": {(DOMAIN, vehicle.vin)}, - "name": f'{vehicle.attributes.get("brand")} {vehicle.name}', - "model": vehicle.name, - "manufacturer": vehicle.attributes.get("brand"), } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, vehicle.vin)}, + manufacturer=vehicle.attributes.get("brand"), + model=vehicle.name, + name=f'{vehicle.attributes.get("brand")} {vehicle.name}', + ) - def update_callback(self): + def update_callback(self) -> None: """Schedule a state update.""" self.schedule_update_ha_state(True) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Add callback after being added to hass. Show latest data after startup. diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index a7fd72fc1a7..7ed23f72389 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -1,155 +1,254 @@ """Reads vehicle status from BMW connected drive portal.""" -import logging +from __future__ import annotations -from bimmer_connected.state import ChargingState, LockState +from collections.abc import Callable +from dataclasses import dataclass +import logging +from typing import Any, cast + +from bimmer_connected.state import ChargingState, LockState, VehicleState +from bimmer_connected.vehicle import ConnectedDriveVehicle +from bimmer_connected.vehicle_status import ConditionBasedServiceReport from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OPENING, DEVICE_CLASS_PLUG, DEVICE_CLASS_PROBLEM, BinarySensorEntity, + BinarySensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import LENGTH_KILOMETERS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.unit_system import UnitSystem -from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from . import ( + DOMAIN as BMW_DOMAIN, + BMWConnectedDriveAccount, + BMWConnectedDriveBaseEntity, +) from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES = { - "lids": ["Doors", DEVICE_CLASS_OPENING, "mdi:car-door-lock"], - "windows": ["Windows", DEVICE_CLASS_OPENING, "mdi:car-door"], - "door_lock_state": ["Door lock state", "lock", "mdi:car-key"], - "lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"], - "condition_based_services": [ - "Condition based services", - DEVICE_CLASS_PROBLEM, - "mdi:wrench", - ], - "check_control_messages": [ - "Control messages", - DEVICE_CLASS_PROBLEM, - "mdi:car-tire-alert", - ], -} -SENSOR_TYPES_ELEC = { - "charging_status": ["Charging status", "power", "mdi:ev-station"], - "connection_status": ["Connection status", DEVICE_CLASS_PLUG, "mdi:car-electric"], -} - -SENSOR_TYPES_ELEC.update(SENSOR_TYPES) +def _are_doors_closed( + vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class opening: On means open, Off means closed + _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) + for lid in vehicle_state.lids: + extra_attributes[lid.name] = lid.state.value + return not vehicle_state.all_lids_closed -async def async_setup_entry(hass, config_entry, async_add_entities): +def _are_windows_closed( + vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class opening: On means open, Off means closed + for window in vehicle_state.windows: + extra_attributes[window.name] = window.state.value + return not vehicle_state.all_windows_closed + + +def _are_doors_locked( + vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class lock: On means unlocked, Off means locked + # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED + extra_attributes["door_lock_state"] = vehicle_state.door_lock_state.value + extra_attributes["last_update_reason"] = vehicle_state.last_update_reason + return vehicle_state.door_lock_state not in {LockState.LOCKED, LockState.SECURED} + + +def _are_parking_lights_on( + vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class light: On means light detected, Off means no light + extra_attributes["lights_parking"] = vehicle_state.parking_lights.value + return cast(bool, vehicle_state.are_parking_lights_on) + + +def _are_problems_detected( + vehicle_state: VehicleState, + extra_attributes: dict[str, Any], + unit_system: UnitSystem, +) -> bool: + # device class problem: On means problem detected, Off means no problem + for report in vehicle_state.condition_based_services: + extra_attributes.update(_format_cbs_report(report, unit_system)) + return not vehicle_state.are_all_cbs_ok + + +def _check_control_messages( + vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class problem: On means problem detected, Off means no problem + check_control_messages = vehicle_state.check_control_messages + has_check_control_messages = vehicle_state.has_check_control_messages + if has_check_control_messages: + cbs_list = [message.description_short for message in check_control_messages] + extra_attributes["check_control_messages"] = cbs_list + else: + extra_attributes["check_control_messages"] = "OK" + return cast(bool, vehicle_state.has_check_control_messages) + + +def _is_vehicle_charging( + vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class power: On means power detected, Off means no power + extra_attributes["charging_status"] = vehicle_state.charging_status.value + extra_attributes[ + "last_charging_end_result" + ] = vehicle_state.last_charging_end_result + return cast(bool, vehicle_state.charging_status == ChargingState.CHARGING) + + +def _is_vehicle_plugged_in( + vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class plug: On means device is plugged in, + # Off means device is unplugged + extra_attributes["connection_status"] = vehicle_state.connection_status + return cast(str, vehicle_state.connection_status) == "CONNECTED" + + +def _format_cbs_report( + report: ConditionBasedServiceReport, unit_system: UnitSystem +) -> dict[str, Any]: + result: dict[str, Any] = {} + service_type = report.service_type.lower().replace("_", " ") + result[f"{service_type} status"] = report.state.value + if report.due_date is not None: + result[f"{service_type} date"] = report.due_date.strftime("%Y-%m-%d") + if report.due_distance is not None: + distance = round(unit_system.length(report.due_distance, LENGTH_KILOMETERS)) + result[f"{service_type} distance"] = f"{distance} {unit_system.length_unit}" + return result + + +@dataclass +class BMWRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[VehicleState, dict[str, Any], UnitSystem], bool] + + +@dataclass +class BMWBinarySensorEntityDescription( + BinarySensorEntityDescription, BMWRequiredKeysMixin +): + """Describes BMW binary_sensor entity.""" + + +SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( + BMWBinarySensorEntityDescription( + key="lids", + name="Doors", + device_class=DEVICE_CLASS_OPENING, + icon="mdi:car-door-lock", + value_fn=_are_doors_closed, + ), + BMWBinarySensorEntityDescription( + key="windows", + name="Windows", + device_class=DEVICE_CLASS_OPENING, + icon="mdi:car-door", + value_fn=_are_windows_closed, + ), + BMWBinarySensorEntityDescription( + key="door_lock_state", + name="Door lock state", + device_class="lock", + icon="mdi:car-key", + value_fn=_are_doors_locked, + ), + BMWBinarySensorEntityDescription( + key="lights_parking", + name="Parking lights", + device_class="light", + icon="mdi:car-parking-lights", + value_fn=_are_parking_lights_on, + ), + BMWBinarySensorEntityDescription( + key="condition_based_services", + name="Condition based services", + device_class=DEVICE_CLASS_PROBLEM, + icon="mdi:wrench", + value_fn=_are_problems_detected, + ), + BMWBinarySensorEntityDescription( + key="check_control_messages", + name="Control messages", + device_class=DEVICE_CLASS_PROBLEM, + icon="mdi:car-tire-alert", + value_fn=_check_control_messages, + ), + # electric + BMWBinarySensorEntityDescription( + key="charging_status", + name="Charging status", + device_class="power", + icon="mdi:ev-station", + value_fn=_is_vehicle_charging, + ), + BMWBinarySensorEntityDescription( + key="connection_status", + name="Connection status", + device_class=DEVICE_CLASS_PLUG, + icon="mdi:car-electric", + value_fn=_is_vehicle_plugged_in, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the BMW ConnectedDrive binary sensors from config entry.""" - account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] - entities = [] + account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ + config_entry.entry_id + ][CONF_ACCOUNT] - for vehicle in account.account.vehicles: - if vehicle.has_hv_battery: - _LOGGER.debug("BMW with a high voltage battery") - for key, value in sorted(SENSOR_TYPES_ELEC.items()): - if key in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1], value[2] - ) - entities.append(device) - elif vehicle.has_internal_combustion_engine: - _LOGGER.debug("BMW with an internal combustion engine") - for key, value in sorted(SENSOR_TYPES.items()): - if key in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1], value[2] - ) - entities.append(device) + entities = [ + BMWConnectedDriveSensor(account, vehicle, description, hass.config.units) + for vehicle in account.account.vehicles + for description in SENSOR_TYPES + if description.key in vehicle.available_attributes + ] async_add_entities(entities, True) class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): """Representation of a BMW vehicle binary sensor.""" + entity_description: BMWBinarySensorEntityDescription + def __init__( - self, account, vehicle, attribute: str, sensor_name, device_class, icon - ): + self, + account: BMWConnectedDriveAccount, + vehicle: ConnectedDriveVehicle, + description: BMWBinarySensorEntityDescription, + unit_system: UnitSystem, + ) -> None: """Initialize sensor.""" super().__init__(account, vehicle) + self.entity_description = description + self._unit_system = unit_system - self._attribute = attribute - self._attr_name = f"{vehicle.name} {attribute}" - self._attr_unique_id = f"{vehicle.vin}-{attribute}" - self._sensor_name = sensor_name - self._attr_device_class = device_class - self._attr_icon = icon + self._attr_name = f"{vehicle.name} {description.key}" + self._attr_unique_id = f"{vehicle.vin}-{description.key}" - def update(self): + def update(self) -> None: """Read new state data from the library.""" vehicle_state = self._vehicle.state result = self._attrs.copy() - # device class opening: On means open, Off means closed - if self._attribute == "lids": - _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) - self._attr_is_on = not vehicle_state.all_lids_closed - for lid in vehicle_state.lids: - result[lid.name] = lid.state.value - elif self._attribute == "windows": - self._attr_is_on = not vehicle_state.all_windows_closed - for window in vehicle_state.windows: - result[window.name] = window.state.value - # device class lock: On means unlocked, Off means locked - elif self._attribute == "door_lock_state": - # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED - self._attr_is_on = vehicle_state.door_lock_state not in [ - LockState.LOCKED, - LockState.SECURED, - ] - result["door_lock_state"] = vehicle_state.door_lock_state.value - result["last_update_reason"] = vehicle_state.last_update_reason - # device class light: On means light detected, Off means no light - elif self._attribute == "lights_parking": - self._attr_is_on = vehicle_state.are_parking_lights_on - result["lights_parking"] = vehicle_state.parking_lights.value - # device class problem: On means problem detected, Off means no problem - elif self._attribute == "condition_based_services": - self._attr_is_on = not vehicle_state.are_all_cbs_ok - for report in vehicle_state.condition_based_services: - result.update(self._format_cbs_report(report)) - elif self._attribute == "check_control_messages": - self._attr_is_on = vehicle_state.has_check_control_messages - check_control_messages = vehicle_state.check_control_messages - has_check_control_messages = vehicle_state.has_check_control_messages - if has_check_control_messages: - cbs_list = [] - for message in check_control_messages: - cbs_list.append(message["ccmDescriptionShort"]) - result["check_control_messages"] = cbs_list - else: - result["check_control_messages"] = "OK" - # device class power: On means power detected, Off means no power - elif self._attribute == "charging_status": - self._attr_is_on = vehicle_state.charging_status in [ChargingState.CHARGING] - result["charging_status"] = vehicle_state.charging_status.value - result["last_charging_end_result"] = vehicle_state.last_charging_end_result - # device class plug: On means device is plugged in, - # Off means device is unplugged - elif self._attribute == "connection_status": - self._attr_is_on = vehicle_state.connection_status == "CONNECTED" - result["connection_status"] = vehicle_state.connection_status - + self._attr_is_on = self.entity_description.value_fn( + vehicle_state, result, self._unit_system + ) self._attr_extra_state_attributes = result - - def _format_cbs_report(self, report): - result = {} - service_type = report.service_type.lower().replace("_", " ") - result[f"{service_type} status"] = report.state.value - if report.due_date is not None: - result[f"{service_type} date"] = report.due_date.strftime("%Y-%m-%d") - if report.due_distance is not None: - distance = round( - self.hass.config.units.length(report.due_distance, LENGTH_KILOMETERS) - ) - result[ - f"{service_type} distance" - ] = f"{distance} {self.hass.config.units.length_unit}" - return result diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index c0d71c978ec..838c991edb3 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -1,4 +1,8 @@ """Config flow for BMW ConnectedDrive integration.""" +from __future__ import annotations + +from typing import Any + from bimmer_connected.account import ConnectedDriveAccount from bimmer_connected.country_selector import get_region_from_name import voluptuous as vol @@ -6,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from . import DOMAIN from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_USE_LOCATION @@ -19,7 +24,9 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -43,9 +50,11 @@ class BMWConnectedDriveConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}" @@ -65,13 +74,15 @@ class BMWConnectedDriveConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import(self, user_input): + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Handle import.""" return await self.async_step_user(user_input) @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> BMWConnectedDriveOptionsFlow: """Return a BWM ConnectedDrive option flow.""" return BMWConnectedDriveOptionsFlow(config_entry) @@ -79,16 +90,20 @@ class BMWConnectedDriveConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class BMWConnectedDriveOptionsFlow(config_entries.OptionsFlow): """Handle a option flow for BMW ConnectedDrive.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize BMW ConnectedDrive option flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) - 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.""" return await self.async_step_account_options() - async def async_step_account_options(self, user_input=None): + async def async_step_account_options( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index c788051dc9a..4d7b6094968 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -1,19 +1,37 @@ """Device tracker for BMW Connected Drive vehicles.""" +from __future__ import annotations + import logging +from typing import Literal + +from bimmer_connected.vehicle import ConnectedDriveVehicle 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.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from . import ( + DOMAIN as BMW_DOMAIN, + BMWConnectedDriveAccount, + BMWConnectedDriveBaseEntity, +) from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the BMW ConnectedDrive tracker from config entry.""" - account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] - entities = [] + account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ + config_entry.entry_id + ][CONF_ACCOUNT] + entities: list[BMWDeviceTracker] = [] for vehicle in account.account.vehicles: entities.append(BMWDeviceTracker(account, vehicle)) @@ -32,36 +50,38 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): _attr_force_update = False _attr_icon = "mdi:car" - def __init__(self, account, vehicle): + def __init__( + self, + account: BMWConnectedDriveAccount, + vehicle: ConnectedDriveVehicle, + ) -> None: """Initialize the Tracker.""" super().__init__(account, vehicle) self._attr_unique_id = vehicle.vin - self._location = ( - vehicle.state.gps_position if vehicle.state.gps_position else (None, None) - ) + self._location = pos if (pos := vehicle.state.gps_position) else None self._attr_name = vehicle.name @property - def latitude(self): + def latitude(self) -> float | None: """Return latitude value of the device.""" return self._location[0] if self._location else None @property - def longitude(self): + def longitude(self) -> float | None: """Return longitude value of the device.""" return self._location[1] if self._location else None @property - def source_type(self): + def source_type(self) -> Literal["gps"]: """Return the source type, eg gps or router, of the device.""" return SOURCE_TYPE_GPS - def update(self): + def update(self) -> None: """Update state of the decvice tracker.""" self._attr_extra_state_attributes = self._attrs self._location = ( self._vehicle.state.gps_position if self._vehicle.state.is_vehicle_tracking_enabled - else (None, None) + else None ) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 3d27cf833b6..62e42476812 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -1,33 +1,54 @@ """Support for BMW car locks with BMW ConnectedDrive.""" import logging +from typing import Any from bimmer_connected.state import LockState +from bimmer_connected.vehicle import ConnectedDriveVehicle from homeassistant.components.lock import LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from . import ( + DOMAIN as BMW_DOMAIN, + BMWConnectedDriveAccount, + BMWConnectedDriveBaseEntity, +) from .const import CONF_ACCOUNT, DATA_ENTRIES DOOR_LOCK_STATE = "door_lock_state" _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the BMW ConnectedDrive binary sensors from config entry.""" - account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] - entities = [] + account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ + config_entry.entry_id + ][CONF_ACCOUNT] if not account.read_only: - for vehicle in account.account.vehicles: - device = BMWLock(account, vehicle, "lock", "BMW lock") - entities.append(device) - async_add_entities(entities, True) + entities = [ + BMWLock(account, vehicle, "lock", "BMW lock") + for vehicle in account.account.vehicles + ] + async_add_entities(entities, True) class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): """Representation of a BMW vehicle lock.""" - def __init__(self, account, vehicle, attribute: str, sensor_name): + def __init__( + self, + account: BMWConnectedDriveAccount, + vehicle: ConnectedDriveVehicle, + attribute: str, + sensor_name: str, + ) -> None: """Initialize the lock.""" super().__init__(account, vehicle) @@ -37,7 +58,7 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): self._sensor_name = sensor_name self.door_lock_state_available = DOOR_LOCK_STATE in vehicle.available_attributes - def lock(self, **kwargs): + def lock(self, **kwargs: Any) -> None: """Lock the car.""" _LOGGER.debug("%s: locking doors", self._vehicle.name) # Optimistic state set here because it takes some time before the @@ -46,7 +67,7 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): self.schedule_update_ha_state() self._vehicle.remote_services.trigger_remote_door_lock() - def unlock(self, **kwargs): + def unlock(self, **kwargs: Any) -> None: """Unlock the car.""" _LOGGER.debug("%s: unlocking doors", self._vehicle.name) # Optimistic state set here because it takes some time before the @@ -55,17 +76,18 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): self.schedule_update_ha_state() self._vehicle.remote_services.trigger_remote_door_unlock() - def update(self): + def update(self) -> None: """Update state of the lock.""" _LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute) - if self._vehicle.state.door_lock_state in [LockState.LOCKED, LockState.SECURED]: - self._attr_is_locked = True - else: - self._attr_is_locked = False + vehicle_state = self._vehicle.state if not self.door_lock_state_available: self._attr_is_locked = None + else: + self._attr_is_locked = vehicle_state.door_lock_state in { + LockState.LOCKED, + LockState.SECURED, + } - vehicle_state = self._vehicle.state result = self._attrs.copy() if self.door_lock_state_available: result["door_lock_state"] = vehicle_state.door_lock_state.value diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 8a1e7e2c826..110f8295c8a 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.7.20"], + "requirements": ["bimmer_connected==0.7.21"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/bmw_connected_drive/notify.py b/homeassistant/components/bmw_connected_drive/notify.py index 3fd40f3801c..4f4645bf78a 100644 --- a/homeassistant/components/bmw_connected_drive/notify.py +++ b/homeassistant/components/bmw_connected_drive/notify.py @@ -1,5 +1,10 @@ """Support for BMW notifications.""" +from __future__ import annotations + import logging +from typing import Any, cast + +from bimmer_connected.vehicle import ConnectedDriveVehicle from homeassistant.components.notify import ( ATTR_DATA, @@ -9,8 +14,10 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.const import ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, ATTR_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as BMW_DOMAIN +from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveAccount from .const import CONF_ACCOUNT, DATA_ENTRIES ATTR_LAT = "lat" @@ -22,9 +29,15 @@ ATTR_TEXT = "text" _LOGGER = logging.getLogger(__name__) -def get_service(hass, config, discovery_info=None): +def get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> BMWNotificationService: """Get the BMW notification service.""" - accounts = [e[CONF_ACCOUNT] for e in hass.data[BMW_DOMAIN][DATA_ENTRIES].values()] + accounts: list[BMWConnectedDriveAccount] = [ + e[CONF_ACCOUNT] for e in hass.data[BMW_DOMAIN][DATA_ENTRIES].values() + ] _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) svc = BMWNotificationService() svc.setup(accounts) @@ -34,22 +47,23 @@ def get_service(hass, config, discovery_info=None): class BMWNotificationService(BaseNotificationService): """Send Notifications to BMW.""" - def __init__(self): + def __init__(self) -> None: """Set up the notification service.""" - self.targets = {} + self.targets: dict[str, ConnectedDriveVehicle] = {} - def setup(self, accounts): + def setup(self, accounts: list[BMWConnectedDriveAccount]) -> None: """Get the BMW vehicle(s) for the account(s).""" for account in accounts: self.targets.update({v.name: v for v in account.account.vehicles}) - def send_message(self, message="", **kwargs): + def send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message or POI to the car.""" - for _vehicle in kwargs[ATTR_TARGET]: - _LOGGER.debug("Sending message to %s", _vehicle.name) + for vehicle in kwargs[ATTR_TARGET]: + vehicle = cast(ConnectedDriveVehicle, vehicle) + _LOGGER.debug("Sending message to %s", vehicle.name) # Extract params from data dict - title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + title: str = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) # Check if message is a POI @@ -68,8 +82,8 @@ class BMWNotificationService(BaseNotificationService): } ) - _vehicle.remote_services.trigger_send_poi(location_dict) + vehicle.remote_services.trigger_send_poi(location_dict) else: - _vehicle.remote_services.trigger_send_message( + vehicle.remote_services.trigger_send_message( {ATTR_TEXT: message, ATTR_SUBJECT: title} ) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 104a2eb78d9..4f0dc7904fc 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,10 +1,16 @@ """Support for reading vehicle status from BMW connected drive portal.""" +from __future__ import annotations + +from copy import copy +from dataclasses import dataclass import logging from bimmer_connected.const import SERVICE_ALL_TRIPS, SERVICE_LAST_TRIP, SERVICE_STATUS from bimmer_connected.state import ChargingState +from bimmer_connected.vehicle import ConnectedDriveVehicle -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, DEVICE_CLASS_TIMESTAMP, @@ -19,431 +25,405 @@ from homeassistant.const import ( VOLUME_GALLONS, VOLUME_LITERS, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import UnitSystem -from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from . import ( + DOMAIN as BMW_DOMAIN, + BMWConnectedDriveAccount, + BMWConnectedDriveBaseEntity, +) from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) -ATTR_TO_HA_METRIC = { - # "": [, , , ], - "mileage": ["mdi:speedometer", None, LENGTH_KILOMETERS, True], - "remaining_range_total": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], - "remaining_range_electric": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - True, - ], - "remaining_range_fuel": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], - "max_range_electric": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], - "remaining_fuel": ["mdi:gas-station", None, VOLUME_LITERS, True], - # LastTrip attributes - "average_combined_consumption": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - True, - ], - "average_electric_consumption": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - True, - ], - "average_recuperation": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - True, - ], - "electric_distance": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], - "saved_fuel": ["mdi:fuel", None, VOLUME_LITERS, False], - "total_distance": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], - # AllTrips attributes - "average_combined_consumption_community_average": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_combined_consumption_community_high": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_combined_consumption_community_low": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_combined_consumption_user_average": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - True, - ], - "average_electric_consumption_community_average": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_electric_consumption_community_high": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_electric_consumption_community_low": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_electric_consumption_user_average": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - True, - ], - "average_recuperation_community_average": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_recuperation_community_high": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_recuperation_community_low": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_recuperation_user_average": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - True, - ], - "chargecycle_range_community_average": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "chargecycle_range_community_high": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "chargecycle_range_community_low": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "chargecycle_range_user_average": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - True, - ], - "chargecycle_range_user_current_charge_cycle": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - True, - ], - "chargecycle_range_user_high": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - True, - ], - "total_electric_distance_community_average": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "total_electric_distance_community_high": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "total_electric_distance_community_low": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "total_electric_distance_user_average": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "total_electric_distance_user_total": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "total_saved_fuel": ["mdi:fuel", None, VOLUME_LITERS, False], -} -ATTR_TO_HA_IMPERIAL = { - # "": [, , , ], - "mileage": ["mdi:speedometer", None, LENGTH_MILES, True], - "remaining_range_total": ["mdi:map-marker-distance", None, LENGTH_MILES, True], - "remaining_range_electric": ["mdi:map-marker-distance", None, LENGTH_MILES, True], - "remaining_range_fuel": ["mdi:map-marker-distance", None, LENGTH_MILES, True], - "max_range_electric": ["mdi:map-marker-distance", None, LENGTH_MILES, True], - "remaining_fuel": ["mdi:gas-station", None, VOLUME_GALLONS, True], - # LastTrip attributes - "average_combined_consumption": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, - ], - "average_electric_consumption": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, - ], - "average_recuperation": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, - ], - "electric_distance": ["mdi:map-marker-distance", None, LENGTH_MILES, True], - "saved_fuel": ["mdi:fuel", None, VOLUME_GALLONS, False], - "total_distance": ["mdi:map-marker-distance", None, LENGTH_MILES, True], - # AllTrips attributes - "average_combined_consumption_community_average": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_combined_consumption_community_high": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_combined_consumption_community_low": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_combined_consumption_user_average": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, - ], - "average_electric_consumption_community_average": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_electric_consumption_community_high": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_electric_consumption_community_low": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_electric_consumption_user_average": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, - ], - "average_recuperation_community_average": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_recuperation_community_high": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_recuperation_community_low": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_recuperation_user_average": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, - ], - "chargecycle_range_community_average": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "chargecycle_range_community_high": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "chargecycle_range_community_low": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "chargecycle_range_user_average": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - True, - ], - "chargecycle_range_user_current_charge_cycle": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - True, - ], - "chargecycle_range_user_high": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - True, - ], - "total_electric_distance_community_average": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "total_electric_distance_community_high": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "total_electric_distance_community_low": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "total_electric_distance_user_average": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "total_electric_distance_user_total": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "total_saved_fuel": ["mdi:fuel", None, VOLUME_GALLONS, False], -} +@dataclass +class BMWSensorEntityDescription(SensorEntityDescription): + """Describes BMW sensor entity.""" -ATTR_TO_HA_GENERIC = { - # "": [, , , ], - "charging_time_remaining": ["mdi:update", None, TIME_HOURS, True], - "charging_status": ["mdi:battery-charging", None, None, True], + unit_metric: str | None = None + unit_imperial: str | None = None + + +SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { + # --- Generic --- + "charging_time_remaining": BMWSensorEntityDescription( + key="charging_time_remaining", + icon="mdi:update", + unit_metric=TIME_HOURS, + unit_imperial=TIME_HOURS, + ), + "charging_status": BMWSensorEntityDescription( + key="charging_status", + icon="mdi:battery-charging", + ), # No icon as this is dealt with directly as a special case in icon() - "charging_level_hv": [None, None, PERCENTAGE, True], + "charging_level_hv": BMWSensorEntityDescription( + key="charging_level_hv", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + ), # LastTrip attributes - "date_utc": [None, DEVICE_CLASS_TIMESTAMP, None, True], - "duration": ["mdi:timer-outline", None, TIME_MINUTES, True], - "electric_distance_ratio": ["mdi:percent-outline", None, PERCENTAGE, False], + "date_utc": BMWSensorEntityDescription( + key="date_utc", + device_class=DEVICE_CLASS_TIMESTAMP, + ), + "duration": BMWSensorEntityDescription( + key="duration", + icon="mdi:timer-outline", + unit_metric=TIME_MINUTES, + unit_imperial=TIME_MINUTES, + ), + "electric_distance_ratio": BMWSensorEntityDescription( + key="electric_distance_ratio", + icon="mdi:percent-outline", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + ), # AllTrips attributes - "battery_size_max": ["mdi:battery-charging-high", None, ENERGY_WATT_HOUR, False], - "reset_date_utc": [None, DEVICE_CLASS_TIMESTAMP, None, False], - "saved_co2": ["mdi:tree-outline", None, MASS_KILOGRAMS, False], - "saved_co2_green_energy": ["mdi:tree-outline", None, MASS_KILOGRAMS, False], + "battery_size_max": BMWSensorEntityDescription( + key="battery_size_max", + icon="mdi:battery-charging-high", + unit_metric=ENERGY_WATT_HOUR, + unit_imperial=ENERGY_WATT_HOUR, + entity_registry_enabled_default=False, + ), + "reset_date_utc": BMWSensorEntityDescription( + key="reset_date_utc", + device_class=DEVICE_CLASS_TIMESTAMP, + entity_registry_enabled_default=False, + ), + "saved_co2": BMWSensorEntityDescription( + key="saved_co2", + icon="mdi:tree-outline", + unit_metric=MASS_KILOGRAMS, + unit_imperial=MASS_KILOGRAMS, + entity_registry_enabled_default=False, + ), + "saved_co2_green_energy": BMWSensorEntityDescription( + key="saved_co2_green_energy", + icon="mdi:tree-outline", + unit_metric=MASS_KILOGRAMS, + unit_imperial=MASS_KILOGRAMS, + entity_registry_enabled_default=False, + ), + # --- Specific --- + "mileage": BMWSensorEntityDescription( + key="mileage", + icon="mdi:speedometer", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + ), + "remaining_range_total": BMWSensorEntityDescription( + key="remaining_range_total", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + ), + "remaining_range_electric": BMWSensorEntityDescription( + key="remaining_range_electric", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + ), + "remaining_range_fuel": BMWSensorEntityDescription( + key="remaining_range_fuel", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + ), + "max_range_electric": BMWSensorEntityDescription( + key="max_range_electric", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + ), + "remaining_fuel": BMWSensorEntityDescription( + key="remaining_fuel", + icon="mdi:gas-station", + unit_metric=VOLUME_LITERS, + unit_imperial=VOLUME_GALLONS, + ), + # LastTrip attributes + "average_combined_consumption": BMWSensorEntityDescription( + key="average_combined_consumption", + icon="mdi:flash", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + ), + "average_electric_consumption": BMWSensorEntityDescription( + key="average_electric_consumption", + icon="mdi:power-plug-outline", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + ), + "average_recuperation": BMWSensorEntityDescription( + key="average_recuperation", + icon="mdi:recycle-variant", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + ), + "electric_distance": BMWSensorEntityDescription( + key="electric_distance", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + ), + "saved_fuel": BMWSensorEntityDescription( + key="saved_fuel", + icon="mdi:fuel", + unit_metric=VOLUME_LITERS, + unit_imperial=VOLUME_GALLONS, + entity_registry_enabled_default=False, + ), + "total_distance": BMWSensorEntityDescription( + key="total_distance", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + ), + # AllTrips attributes + "average_combined_consumption_community_average": BMWSensorEntityDescription( + key="average_combined_consumption_community_average", + icon="mdi:flash", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, + ), + "average_combined_consumption_community_high": BMWSensorEntityDescription( + key="average_combined_consumption_community_high", + icon="mdi:flash", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, + ), + "average_combined_consumption_community_low": BMWSensorEntityDescription( + key="average_combined_consumption_community_low", + icon="mdi:flash", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, + ), + "average_combined_consumption_user_average": BMWSensorEntityDescription( + key="average_combined_consumption_user_average", + icon="mdi:flash", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + ), + "average_electric_consumption_community_average": BMWSensorEntityDescription( + key="average_electric_consumption_community_average", + icon="mdi:power-plug-outline", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, + ), + "average_electric_consumption_community_high": BMWSensorEntityDescription( + key="average_electric_consumption_community_high", + icon="mdi:power-plug-outline", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, + ), + "average_electric_consumption_community_low": BMWSensorEntityDescription( + key="average_electric_consumption_community_low", + icon="mdi:power-plug-outline", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, + ), + "average_electric_consumption_user_average": BMWSensorEntityDescription( + key="average_electric_consumption_user_average", + icon="mdi:power-plug-outline", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + ), + "average_recuperation_community_average": BMWSensorEntityDescription( + key="average_recuperation_community_average", + icon="mdi:recycle-variant", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, + ), + "average_recuperation_community_high": BMWSensorEntityDescription( + key="average_recuperation_community_high", + icon="mdi:recycle-variant", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, + ), + "average_recuperation_community_low": BMWSensorEntityDescription( + key="average_recuperation_community_low", + icon="mdi:recycle-variant", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, + ), + "average_recuperation_user_average": BMWSensorEntityDescription( + key="average_recuperation_user_average", + icon="mdi:recycle-variant", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + ), + "chargecycle_range_community_average": BMWSensorEntityDescription( + key="chargecycle_range_community_average", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, + ), + "chargecycle_range_community_high": BMWSensorEntityDescription( + key="chargecycle_range_community_high", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, + ), + "chargecycle_range_community_low": BMWSensorEntityDescription( + key="chargecycle_range_community_low", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, + ), + "chargecycle_range_user_average": BMWSensorEntityDescription( + key="chargecycle_range_user_average", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + ), + "chargecycle_range_user_current_charge_cycle": BMWSensorEntityDescription( + key="chargecycle_range_user_current_charge_cycle", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + ), + "chargecycle_range_user_high": BMWSensorEntityDescription( + key="chargecycle_range_user_high", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + ), + "total_electric_distance_community_average": BMWSensorEntityDescription( + key="total_electric_distance_community_average", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, + ), + "total_electric_distance_community_high": BMWSensorEntityDescription( + key="total_electric_distance_community_high", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, + ), + "total_electric_distance_community_low": BMWSensorEntityDescription( + key="total_electric_distance_community_low", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, + ), + "total_electric_distance_user_average": BMWSensorEntityDescription( + key="total_electric_distance_user_average", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, + ), + "total_electric_distance_user_total": BMWSensorEntityDescription( + key="total_electric_distance_user_total", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, + ), + "total_saved_fuel": BMWSensorEntityDescription( + key="total_saved_fuel", + icon="mdi:fuel", + unit_metric=VOLUME_LITERS, + unit_imperial=VOLUME_GALLONS, + entity_registry_enabled_default=False, + ), } -ATTR_TO_HA_METRIC.update(ATTR_TO_HA_GENERIC) -ATTR_TO_HA_IMPERIAL.update(ATTR_TO_HA_GENERIC) + +DEFAULT_BMW_DESCRIPTION = BMWSensorEntityDescription( + key="", + entity_registry_enabled_default=True, +) -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 BMW ConnectedDrive sensors from config entry.""" # pylint: disable=too-many-nested-blocks - if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - attribute_info = ATTR_TO_HA_IMPERIAL - else: - attribute_info = ATTR_TO_HA_METRIC - - account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] - entities = [] + unit_system = hass.config.units + account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ + config_entry.entry_id + ][CONF_ACCOUNT] + entities: list[BMWConnectedDriveSensor] = [] for vehicle in account.account.vehicles: for service in vehicle.available_state_services: if service == SERVICE_STATUS: - for attribute_name in vehicle.drive_train_attributes: - if attribute_name in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info + entities.extend( + [ + BMWConnectedDriveSensor( + account, vehicle, description, unit_system ) - entities.append(device) + for attribute_name in vehicle.drive_train_attributes + if attribute_name in vehicle.available_attributes + and (description := SENSOR_TYPES.get(attribute_name)) + ] + ) if service == SERVICE_LAST_TRIP: - for attribute_name in vehicle.state.last_trip.available_attributes: - if attribute_name == "date": - device = BMWConnectedDriveSensor( + entities.extend( + [ + # mypy issues will be fixed in next release + # https://github.com/python/mypy/issues/9096 + BMWConnectedDriveSensor( account, vehicle, - "date_utc", - attribute_info, + description, # type: ignore[arg-type] + unit_system, service, ) - entities.append(device) - else: - device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info, service + for attribute_name in vehicle.state.last_trip.available_attributes + if attribute_name != "date" + and (description := SENSOR_TYPES.get(attribute_name)) # type: ignore[no-redef] + ] + ) + if "date" in vehicle.state.last_trip.available_attributes: + entities.append( + BMWConnectedDriveSensor( + account, + vehicle, + SENSOR_TYPES["date_utc"], + unit_system, + service, ) - entities.append(device) + ) if service == SERVICE_ALL_TRIPS: for attribute_name in vehicle.state.all_trips.available_attributes: if attribute_name == "reset_date": - device = BMWConnectedDriveSensor( - account, - vehicle, - "reset_date_utc", - attribute_info, - service, + entities.append( + BMWConnectedDriveSensor( + account, + vehicle, + SENSOR_TYPES["reset_date_utc"], + unit_system, + service, + ) ) - entities.append(device) elif attribute_name in ( "average_combined_consumption", "average_electric_consumption", @@ -451,45 +431,60 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "chargecycle_range", "total_electric_distance", ): - for attr in ( - "community_average", - "community_high", - "community_low", - "user_average", - ): - device = BMWConnectedDriveSensor( + entities.extend( + [ + BMWConnectedDriveSensor( + account, + vehicle, + SENSOR_TYPES[f"{attribute_name}_{attr}"], + unit_system, + service, + ) + for attr in ( + "community_average", + "community_high", + "community_low", + "user_average", + ) + ] + ) + if attribute_name == "chargecycle_range": + entities.extend( + BMWConnectedDriveSensor( + account, + vehicle, + SENSOR_TYPES[f"{attribute_name}_{attr}"], + unit_system, + service, + ) + for attr in ("user_current_charge_cycle", "user_high") + ) + elif attribute_name == "total_electric_distance": + entities.extend( + [ + BMWConnectedDriveSensor( + account, + vehicle, + SENSOR_TYPES[f"{attribute_name}_{attr}"], + unit_system, + service, + ) + for attr in ("user_total",) + ] + ) + else: + if (description := SENSOR_TYPES.get(attribute_name)) is None: + description = copy(DEFAULT_BMW_DESCRIPTION) + description.key = attribute_name + entities.append( + BMWConnectedDriveSensor( account, vehicle, - f"{attribute_name}_{attr}", - attribute_info, + description, + unit_system, service, ) - entities.append(device) - if attribute_name == "chargecycle_range": - for attr in ("user_current_charge_cycle", "user_high"): - device = BMWConnectedDriveSensor( - account, - vehicle, - f"{attribute_name}_{attr}", - attribute_info, - service, - ) - entities.append(device) - if attribute_name == "total_electric_distance": - for attr in ("user_total",): - device = BMWConnectedDriveSensor( - account, - vehicle, - f"{attribute_name}_{attr}", - attribute_info, - service, - ) - entities.append(device) - else: - device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info, service ) - entities.append(device) async_add_entities(entities, True) @@ -497,55 +492,64 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): """Representation of a BMW vehicle sensor.""" - def __init__(self, account, vehicle, attribute: str, attribute_info, service=None): + entity_description: BMWSensorEntityDescription + + def __init__( + self, + account: BMWConnectedDriveAccount, + vehicle: ConnectedDriveVehicle, + description: BMWSensorEntityDescription, + unit_system: UnitSystem, + service: str | None = None, + ) -> None: """Initialize BMW vehicle sensor.""" super().__init__(account, vehicle) + self.entity_description = description - self._attribute = attribute self._service = service if service: - self._attr_name = f"{vehicle.name} {service.lower()}_{attribute}" - self._attr_unique_id = f"{vehicle.vin}-{service.lower()}-{attribute}" + self._attr_name = f"{vehicle.name} {service.lower()}_{description.key}" + self._attr_unique_id = f"{vehicle.vin}-{service.lower()}-{description.key}" else: - self._attr_name = f"{vehicle.name} {attribute}" - self._attr_unique_id = f"{vehicle.vin}-{attribute}" - self._attribute_info = attribute_info - self._attr_entity_registry_enabled_default = attribute_info.get( - attribute, [None, None, None, True] - )[3] - self._attr_icon = self._attribute_info.get( - self._attribute, [None, None, None, None] - )[0] - self._attr_device_class = attribute_info.get( - attribute, [None, None, None, None] - )[1] - self._attr_native_unit_of_measurement = attribute_info.get( - attribute, [None, None, None, None] - )[2] + self._attr_name = f"{vehicle.name} {description.key}" + self._attr_unique_id = f"{vehicle.vin}-{description.key}" + + if unit_system.name == CONF_UNIT_SYSTEM_IMPERIAL: + self._attr_native_unit_of_measurement = description.unit_imperial + else: + self._attr_native_unit_of_measurement = description.unit_metric def update(self) -> None: """Read new state data from the library.""" _LOGGER.debug("Updating %s", self._vehicle.name) vehicle_state = self._vehicle.state - if self._attribute == "charging_status": - self._attr_native_value = getattr(vehicle_state, self._attribute).value + sensor_key = self.entity_description.key + if sensor_key == "charging_status": + self._attr_native_value = getattr(vehicle_state, sensor_key).value elif self.unit_of_measurement == VOLUME_GALLONS: - value = getattr(vehicle_state, self._attribute) + value = getattr(vehicle_state, sensor_key) value_converted = self.hass.config.units.volume(value, VOLUME_LITERS) self._attr_native_value = round(value_converted) elif self.unit_of_measurement == LENGTH_MILES: - value = getattr(vehicle_state, self._attribute) + value = getattr(vehicle_state, sensor_key) value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS) self._attr_native_value = round(value_converted) elif self._service is None: - self._attr_native_value = getattr(vehicle_state, self._attribute) + self._attr_native_value = getattr(vehicle_state, sensor_key) elif self._service == SERVICE_LAST_TRIP: vehicle_last_trip = self._vehicle.state.last_trip - if self._attribute == "date_utc": + if sensor_key == "date_utc": date_str = getattr(vehicle_last_trip, "date") - self._attr_native_value = dt_util.parse_datetime(date_str).isoformat() + if parsed_date := dt_util.parse_datetime(date_str): + self._attr_native_value = parsed_date.isoformat() + else: + _LOGGER.debug( + "Could not parse date string for 'date_utc' sensor: %s", + date_str, + ) + self._attr_native_value = None else: - self._attr_native_value = getattr(vehicle_last_trip, self._attribute) + self._attr_native_value = getattr(vehicle_last_trip, sensor_key) elif self._service == SERVICE_ALL_TRIPS: vehicle_all_trips = self._vehicle.state.all_trips for attribute in ( @@ -555,21 +559,28 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): "chargecycle_range", "total_electric_distance", ): - if self._attribute.startswith(f"{attribute}_"): + if sensor_key.startswith(f"{attribute}_"): attr = getattr(vehicle_all_trips, attribute) - sub_attr = self._attribute.replace(f"{attribute}_", "") + sub_attr = sensor_key.replace(f"{attribute}_", "") self._attr_native_value = getattr(attr, sub_attr) return - if self._attribute == "reset_date_utc": + if sensor_key == "reset_date_utc": date_str = getattr(vehicle_all_trips, "reset_date") - self._attr_native_value = dt_util.parse_datetime(date_str).isoformat() + if parsed_date := dt_util.parse_datetime(date_str): + self._attr_native_value = parsed_date.isoformat() + else: + _LOGGER.debug( + "Could not parse date string for 'reset_date_utc' sensor: %s", + date_str, + ) + self._attr_native_value = None else: - self._attr_native_value = getattr(vehicle_all_trips, self._attribute) + self._attr_native_value = getattr(vehicle_all_trips, sensor_key) vehicle_state = self._vehicle.state - charging_state = vehicle_state.charging_status in [ChargingState.CHARGING] + charging_state = vehicle_state.charging_status in {ChargingState.CHARGING} - if self._attribute == "charging_level_hv": + if sensor_key == "charging_level_hv": self._attr_icon = icon_for_battery_level( battery_level=vehicle_state.charging_level_hv, charging=charging_state ) diff --git a/homeassistant/components/bmw_connected_drive/services.yaml b/homeassistant/components/bmw_connected_drive/services.yaml index 964fb8ab39b..3f5ff76bdd3 100644 --- a/homeassistant/components/bmw_connected_drive/services.yaml +++ b/homeassistant/components/bmw_connected_drive/services.yaml @@ -68,6 +68,28 @@ activate_air_conditioning: selector: text: +deactivate_air_conditioning: + name: Deactivate air conditioning + description: > + Stops the air conditioning of the vehicle. This only works on newer vehicles if you also + have the option in the MyBMW app. 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 + advanced: true + required: false + example: WBANXXXXXX1234567 + selector: + text: + find_vehicle: name: Find vehicle description: > diff --git a/homeassistant/components/bmw_connected_drive/translations/bg.json b/homeassistant/components/bmw_connected_drive/translations/bg.json index 67a484573aa..90901301faf 100644 --- a/homeassistant/components/bmw_connected_drive/translations/bg.json +++ b/homeassistant/components/bmw_connected_drive/translations/bg.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json b/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json index fde5e1e3c94..4f62df586f5 100644 --- a/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json +++ b/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json @@ -21,7 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "\u552f\u8b80\uff08\u50c5\u652f\u63f4\u50b3\u611f\u5668\u8207\u901a\u77e5\uff0c\u4e0d\n\u5305\u542b\u670d\u52d9\u8207\u9396\u5b9a\uff09", + "read_only": "\u552f\u8b80\uff08\u50c5\u652f\u63f4\u611f\u6e2c\u5668\u8207\u901a\u77e5\uff0c\u4e0d\n\u5305\u542b\u670d\u52d9\u8207\u9396\u5b9a\uff09", "use_location": "\u4f7f\u7528 Home Assistant \u4f4d\u7f6e\u53d6\u5f97\u6c7d\u8eca\u4f4d\u7f6e\uff08\u9700\u8981\u70ba2014/7 \u524d\u751f\u7522\u7684\u975ei3/i8 \u8eca\u6b3e\uff09" } } diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 93a927d21f3..20b6b0a2ea5 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -1,23 +1,27 @@ """The Bond integration.""" from asyncio import TimeoutError as AsyncIOTimeoutError +from http import HTTPStatus +import logging +from typing import Any -from aiohttp import ClientError, ClientTimeout +from aiohttp import ClientError, ClientResponseError, ClientTimeout from bond_api import Bond, BPUPSubscriptions, start_bpup from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import SLOW_UPDATE_WARNING -from .const import BPUP_STOP, BPUP_SUBS, BRIDGE_MAKE, DOMAIN, HUB +from .const import BPUP_SUBS, BRIDGE_MAKE, DOMAIN, HUB from .utils import BondHub PLATFORMS = ["cover", "fan", "light", "switch"] _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 -_STOP_CANCEL = "stop_cancel" + +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -35,6 +39,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hub = BondHub(bond) try: await hub.setup() + except ClientResponseError as ex: + if ex.status == HTTPStatus.UNAUTHORIZED: + _LOGGER.error("Bond token no longer valid: %s", ex) + return False + raise ConfigEntryNotReady from ex except (ClientError, AsyncIOTimeoutError, OSError) as error: raise ConfigEntryNotReady from error @@ -42,18 +51,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: stop_bpup = await start_bpup(host, bpup_subs) @callback - def _async_stop_event(event: Event) -> None: + def _async_stop_event(*_: Any) -> None: stop_bpup() - stop_event_cancel = hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, _async_stop_event + entry.async_on_unload(_async_stop_event) + entry.async_on_unload( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_stop_event) ) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { HUB: hub, BPUP_SUBS: bpup_subs, - BPUP_STOP: stop_bpup, - _STOP_CANCEL: stop_event_cancel, } if not entry.unique_id: @@ -82,15 +90,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - data = hass.data[DOMAIN][entry.entry_id] - data[_STOP_CANCEL]() - if BPUP_STOP in data: - data[BPUP_STOP]() - if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 9285b580851..6f70d37e0a1 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Bond integration.""" from __future__ import annotations +from http import HTTPStatus import logging from typing import Any @@ -9,14 +10,10 @@ from bond_api import Bond import voluptuous as vol from homeassistant import config_entries, exceptions -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_HOST, - CONF_NAME, - HTTP_UNAUTHORIZED, -) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType @@ -33,6 +30,16 @@ DISCOVERY_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) TOKEN_SCHEMA = vol.Schema({}) +async def async_get_token(hass: HomeAssistant, host: str) -> str | None: + """Try to fetch the token from the bond device.""" + bond = Bond(host, "", session=async_get_clientsession(hass)) + try: + response: dict[str, str] = await bond.token() + except ClientConnectionError: + return None + return response.get("token") + + async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[str, str]: """Validate the user input allows us to connect.""" @@ -45,7 +52,7 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[st except ClientConnectionError as error: raise InputValidationError("cannot_connect") from error except ClientResponseError as error: - if error.status == HTTP_UNAUTHORIZED: + if error.status == HTTPStatus.UNAUTHORIZED: raise InputValidationError("invalid_auth") from error raise InputValidationError("unknown") from error except Exception as error: @@ -75,16 +82,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): online longer then the allowed setup period, and we will instead ask them to manually enter the token. """ - bond = Bond( - self._discovered[CONF_HOST], "", session=async_get_clientsession(self.hass) - ) - try: - response = await bond.token() - except ClientConnectionError: - return - - token = response.get("token") - if token is None: + host = self._discovered[CONF_HOST] + if not (token := await async_get_token(self.hass, host)): return self._discovered[CONF_ACCESS_TOKEN] = token @@ -99,7 +98,23 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): host: str = discovery_info[CONF_HOST] bond_id = name.partition(".")[0] await self.async_set_unique_id(bond_id) - self._abort_if_unique_id_configured({CONF_HOST: host}) + for entry in self._async_current_entries(): + if entry.unique_id != bond_id: + continue + updates = {CONF_HOST: host} + if entry.state == ConfigEntryState.SETUP_ERROR and ( + token := await async_get_token(self.hass, host) + ): + updates[CONF_ACCESS_TOKEN] = token + new_data = {**entry.data, **updates} + if new_data != dict(entry.data): + self.hass.config_entries.async_update_entry( + entry, data={**entry.data, **updates} + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + raise AbortFlow("already_configured") self._discovered = {CONF_HOST: host, CONF_NAME: bond_id} await self._async_try_automatic_configure() diff --git a/homeassistant/components/bond/const.py b/homeassistant/components/bond/const.py index 4d886c2ee77..778dcbc1a1f 100644 --- a/homeassistant/components/bond/const.py +++ b/homeassistant/components/bond/const.py @@ -9,7 +9,6 @@ CONF_BOND_ID: str = "bond_id" HUB = "hub" BPUP_SUBS = "bpup_subs" -BPUP_STOP = "bpup_stop" SERVICE_SET_FAN_SPEED_TRACKED_STATE = "set_fan_speed_tracked_state" SERVICE_SET_POWER_TRACKED_STATE = "set_switch_power_tracked_state" diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 3063a3e4efa..5f37de4fa19 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -10,6 +10,13 @@ from typing import Any from aiohttp import ClientError from bond_api import BPUPSubscriptions +from homeassistant.const import ( + ATTR_MODEL, + ATTR_NAME, + ATTR_SUGGESTED_AREA, + ATTR_SW_VERSION, + ATTR_VIA_DEVICE, +) from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval @@ -54,22 +61,22 @@ class BondEntity(Entity): @property def device_info(self) -> DeviceInfo: """Get a an HA device representing this Bond controlled device.""" - device_info: DeviceInfo = { - "manufacturer": self._hub.make, + device_info = DeviceInfo( + manufacturer=self._hub.make, # type ignore: tuple items should not be Optional - "identifiers": {(DOMAIN, self._hub.bond_id, self._device.device_id)}, # type: ignore[arg-type] - } + identifiers={(DOMAIN, self._hub.bond_id, self._device.device_id)}, # type: ignore[arg-type] + ) if self.name is not None: - device_info["name"] = self.name + device_info[ATTR_NAME] = self.name if self._hub.bond_id is not None: - device_info["via_device"] = (DOMAIN, self._hub.bond_id) + device_info[ATTR_VIA_DEVICE] = (DOMAIN, self._hub.bond_id) if self._device.location is not None: - device_info["suggested_area"] = self._device.location + device_info[ATTR_SUGGESTED_AREA] = self._device.location if not self._hub.is_bridge: if self._hub.model is not None: - device_info["model"] = self._hub.model + device_info[ATTR_MODEL] = self._hub.model if self._hub.fw_ver is not None: - device_info["sw_version"] = self._hub.fw_ver + device_info[ATTR_SW_VERSION] = self._hub.fw_ver else: model_data = [] if self._device.branding_profile: @@ -77,7 +84,7 @@ class BondEntity(Entity): if self._device.template: model_data.append(self._device.template) if model_data: - device_info["model"] = " ".join(model_data) + device_info[ATTR_MODEL] = " ".join(model_data) return device_info diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 82bfa24e44d..dd4699ad006 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -273,8 +273,7 @@ class BondFireplace(BondEntity, LightEntity): """Turn the fireplace on.""" _LOGGER.debug("Fireplace async_turn_on called with: %s", kwargs) - brightness = kwargs.get(ATTR_BRIGHTNESS) - if brightness: + if brightness := kwargs.get(ATTR_BRIGHTNESS): flame = round((brightness * 100) / 255) await self._hub.bond.action(self._device.device_id, Action.set_flame(flame)) else: diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 6f11b8c66e3..a8395b68d60 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/bond", "requirements": ["bond-api==0.1.14"], "zeroconf": ["_bond._tcp.local."], - "codeowners": ["@prystupa", "@joshs85"], + "codeowners": ["@bdraco", "@prystupa", "@joshs85"], "quality_scale": "platinum", "iot_class": "local_push" } diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json index c1bac971f4b..179ec599d9f 100644 --- a/homeassistant/components/bond/translations/hu.json +++ b/homeassistant/components/bond/translations/hu.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rj\u00fck friss\u00edtsd, miel\u0151tt folytatn\u00e1d", + "old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rj\u00fck friss\u00edtse, miel\u0151tt folytatn\u00e1", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index a8966ce2f4a..c3a981aa658 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -2,7 +2,7 @@ from boschshcpy.device import SHCDevice from homeassistant.helpers.device_registry import async_get as get_dev_reg -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN @@ -28,18 +28,18 @@ class SHCEntity(Entity): self._entry_id = entry_id self._attr_name = device.name self._attr_unique_id = device.serial - self._attr_device_info = { - "identifiers": {(DOMAIN, device.id)}, - "name": device.name, - "manufacturer": device.manufacturer, - "model": device.device_model, - "via_device": ( + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.id)}, + manufacturer=device.manufacturer, + model=device.device_model, + name=device.name, + via_device=( DOMAIN, device.parent_device_id if device.parent_device_id is not None else parent_id, ), - } + ) async def async_added_to_hass(self): """Subscribe to SHC events.""" diff --git a/homeassistant/components/bosch_shc/translations/bg.json b/homeassistant/components/bosch_shc/translations/bg.json new file mode 100644 index 00000000000..80f917a9793 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/bg.json @@ -0,0 +1,3 @@ +{ + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/hu.json b/homeassistant/components/bosch_shc/translations/hu.json index cf0090475b7..b3e0ed77815 100644 --- a/homeassistant/components/bosch_shc/translations/hu.json +++ b/homeassistant/components/bosch_shc/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/bosch_shc/translations/pl.json b/homeassistant/components/bosch_shc/translations/pl.json index c140bf6b6f8..6b21fd87b7c 100644 --- a/homeassistant/components/bosch_shc/translations/pl.json +++ b/homeassistant/components/bosch_shc/translations/pl.json @@ -18,7 +18,7 @@ }, "credentials": { "data": { - "password": "Has\u0142o kontrolera" + "password": "Has\u0142o" } }, "reauth_confirm": { diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 8528e3649c1..5451738cec6 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -52,12 +52,12 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] unique_id = config_entry.unique_id assert unique_id is not None - device_info: DeviceInfo = { - "identifiers": {(DOMAIN, unique_id)}, - "name": DEFAULT_NAME, - "manufacturer": ATTR_MANUFACTURER, - "model": config_entry.title, - } + device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + model=config_entry.title, + name=DEFAULT_NAME, + ) async_add_entities( [BraviaTVMediaPlayer(coordinator, DEFAULT_NAME, unique_id, device_info)] diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 81761240320..7e01f26d0a5 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -25,12 +25,12 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] unique_id = config_entry.unique_id assert unique_id is not None - device_info: DeviceInfo = { - "identifiers": {(DOMAIN, unique_id)}, - "name": DEFAULT_NAME, - "manufacturer": ATTR_MANUFACTURER, - "model": config_entry.title, - } + device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + model=config_entry.title, + name=DEFAULT_NAME, + ) async_add_entities( [BraviaTVRemote(coordinator, DEFAULT_NAME, unique_id, device_info)] diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json new file mode 100644 index 00000000000..d05511b8d29 --- /dev/null +++ b/homeassistant/components/braviatv/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441", + "unsupported_model": "\u041c\u043e\u0434\u0435\u043b\u044a\u0442 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430." + }, + "step": { + "authorize": { + "data": { + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "Sony Bravia TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index ed56e42342d..f40fd7785a1 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -1,4 +1,5 @@ """Constants for the Broadlink integration.""" +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -33,6 +34,7 @@ DOMAINS_AND_TYPES = { "SP4", "SP4B", }, + LIGHT_DOMAIN: {"LB1"}, } DEFAULT_PORT = 80 diff --git a/homeassistant/components/broadlink/entity.py b/homeassistant/components/broadlink/entity.py index bd2f938a2bd..080cd5bab71 100644 --- a/homeassistant/components/broadlink/entity.py +++ b/homeassistant/components/broadlink/entity.py @@ -1,7 +1,7 @@ """Broadlink entities.""" from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN @@ -51,13 +51,13 @@ class BroadlinkEntity(Entity): return self._device.update_manager.available @property - def device_info(self): + def device_info(self) -> DeviceInfo: """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, - } + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._device.mac_address)}, + 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, + ) diff --git a/homeassistant/components/broadlink/light.py b/homeassistant/components/broadlink/light.py new file mode 100644 index 00000000000..4fc8f4c2120 --- /dev/null +++ b/homeassistant/components/broadlink/light.py @@ -0,0 +1,136 @@ +"""Support for Broadlink lights.""" +import logging + +from broadlink.exceptions import BroadlinkException + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_UNKNOWN, + LightEntity, +) + +from .const import DOMAIN +from .entity import BroadlinkEntity + +_LOGGER = logging.getLogger(__name__) + +BROADLINK_COLOR_MODE_RGB = 0 +BROADLINK_COLOR_MODE_WHITE = 1 +BROADLINK_COLOR_MODE_SCENES = 2 + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Broadlink light.""" + device = hass.data[DOMAIN].devices[config_entry.entry_id] + + if device.api.type == "LB1": + lights = [BroadlinkLight(device)] + + async_add_entities(lights) + + +class BroadlinkLight(BroadlinkEntity, LightEntity): + """Representation of a Broadlink light.""" + + def __init__(self, device): + """Initialize the light.""" + super().__init__(device) + self._attr_name = f"{device.name} Light" + self._attr_unique_id = device.unique_id + self._attr_supported_color_modes = set() + + data = self._coordinator.data + + if {"hue", "saturation"}.issubset(data): + self._attr_supported_color_modes.add(COLOR_MODE_HS) + if "colortemp" in data: + self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + if not self.supported_color_modes: + self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} + + self._update_state(data) + + def _update_state(self, data): + """Update the state of the entity.""" + if "pwr" in data: + self._attr_is_on = bool(data["pwr"]) + + if "brightness" in data: + self._attr_brightness = round(data["brightness"] * 2.55) + + if self.supported_color_modes == {COLOR_MODE_BRIGHTNESS}: + self._attr_color_mode = COLOR_MODE_BRIGHTNESS + return + + if {"hue", "saturation"}.issubset(data): + self._attr_hs_color = [data["hue"], data["saturation"]] + + if "colortemp" in data: + self._attr_color_temp = round((data["colortemp"] - 2700) / 100 + 153) + + if "bulb_colormode" in data: + if data["bulb_colormode"] == BROADLINK_COLOR_MODE_RGB: + self._attr_color_mode = COLOR_MODE_HS + elif data["bulb_colormode"] == BROADLINK_COLOR_MODE_WHITE: + self._attr_color_mode = COLOR_MODE_COLOR_TEMP + else: + # Scenes are not yet supported. + self._attr_color_mode = COLOR_MODE_UNKNOWN + + async def async_turn_on(self, **kwargs): + """Turn on the light.""" + state = {"pwr": 1} + + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + state["brightness"] = round(brightness / 2.55) + + if self.supported_color_modes == {COLOR_MODE_BRIGHTNESS}: + state["bulb_colormode"] = BROADLINK_COLOR_MODE_WHITE + + elif ATTR_HS_COLOR in kwargs: + hs_color = kwargs[ATTR_HS_COLOR] + state["hue"] = int(hs_color[0]) + state["saturation"] = int(hs_color[1]) + state["bulb_colormode"] = BROADLINK_COLOR_MODE_RGB + + elif ATTR_COLOR_TEMP in kwargs: + color_temp = kwargs[ATTR_COLOR_TEMP] + state["colortemp"] = (color_temp - 153) * 100 + 2700 + state["bulb_colormode"] = BROADLINK_COLOR_MODE_WHITE + + elif ATTR_COLOR_MODE in kwargs: + color_mode = kwargs[ATTR_COLOR_MODE] + if color_mode == COLOR_MODE_HS: + state["bulb_colormode"] = BROADLINK_COLOR_MODE_RGB + elif color_mode == COLOR_MODE_COLOR_TEMP: + state["bulb_colormode"] = BROADLINK_COLOR_MODE_WHITE + else: + # Scenes are not yet supported. + state["bulb_colormode"] = BROADLINK_COLOR_MODE_SCENES + + await self._async_set_state(state) + + async def async_turn_off(self, **kwargs): + """Turn off the light.""" + await self._async_set_state({"pwr": 0}) + + async def _async_set_state(self, state): + """Set the state of the light.""" + try: + state = await self._device.async_request( + self._device.api.set_state, **state + ) + except (BroadlinkException, OSError) as err: + _LOGGER.error("Failed to set state: %s", err) + return False + + self._update_state(state) + self.async_write_ha_state() + return True diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index c27b9276ec4..1a6e94003ca 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -2,8 +2,8 @@ "domain": "broadlink", "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", - "requirements": ["broadlink==0.17.0"], - "codeowners": ["@danielhiversen", "@felipediel"], + "requirements": ["broadlink==0.18.0"], + "codeowners": ["@danielhiversen", "@felipediel", "@L-I-Am"], "config_flow": true, "dhcp": [ { diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 676edb53b9a..8f739deeaa8 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -6,16 +6,28 @@ import logging import voluptuous as vol from homeassistant.components.sensor import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) -from homeassistant.const import CONF_HOST, PERCENTAGE, POWER_WATT, TEMP_CELSIUS +from homeassistant.const import ( + CONF_HOST, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + PERCENTAGE, + POWER_WATT, + TEMP_CELSIUS, +) from homeassistant.helpers import config_validation as cv from .const import DOMAIN @@ -59,6 +71,34 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), + SensorEntityDescription( + key="volt", + name="Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="current", + name="Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="overload", + name="Overload", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="totalconsum", + name="Total consumption", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/broadlink/translations/bg.json b/homeassistant/components/broadlink/translations/bg.json new file mode 100644 index 00000000000..7534f7228f9 --- /dev/null +++ b/homeassistant/components/broadlink/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/hu.json b/homeassistant/components/broadlink/translations/hu.json index d3a59a03cea..0bab0c1752f 100644 --- a/homeassistant/components/broadlink/translations/hu.json +++ b/homeassistant/components/broadlink/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", "not_supported": "Az eszk\u00f6z nem t\u00e1mogatott", diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index a84eec07d68..29020b1e905 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -16,6 +16,7 @@ def get_update_manager(device): update_managers = { "A1": BroadlinkA1UpdateManager, "BG1": BroadlinkBG1UpdateManager, + "LB1": BroadlinkLB1UpdateManager, "MP1": BroadlinkMP1UpdateManager, "RM4MINI": BroadlinkRMUpdateManager, "RM4PRO": BroadlinkRMUpdateManager, @@ -175,3 +176,11 @@ class BroadlinkSP4UpdateManager(BroadlinkUpdateManager): async def async_fetch_data(self): """Fetch data from the device.""" return await self.device.async_request(self.device.api.get_state) + + +class BroadlinkLB1UpdateManager(BroadlinkUpdateManager): + """Manages updates for Broadlink LB1 devices.""" + + async def async_fetch_data(self): + """Fetch data from the device.""" + return await self.device.async_request(self.device.api.get_state) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 08daf0155a1..20e5938884f 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -86,12 +86,18 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Hostname is format: brother.local. self.host = discovery_info["hostname"].rstrip(".") - snmp_engine = get_snmp_engine(self.hass) + # Do not probe the device if the host is already configured + self._async_abort_entries_match({CONF_HOST: self.host}) + + snmp_engine = get_snmp_engine(self.hass) + model = discovery_info.get("properties", {}).get("product") - self.brother = Brother(self.host, snmp_engine=snmp_engine) try: + self.brother = Brother(self.host, snmp_engine=snmp_engine, model=model) await self.brother.async_update() - except (ConnectionError, SnmpError, UnsupportedModel): + except UnsupportedModel: + return self.async_abort(reason="unsupported_model") + except (ConnectionError, SnmpError): return self.async_abort(reason="cannot_connect") # Check if already configured diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 8e34f9f983b..a91d84103e1 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -7,7 +7,11 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntityDescription, ) -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE +from homeassistant.const import ( + DEVICE_CLASS_TIMESTAMP, + ENTITY_CATEGORY_DIAGNOSTIC, + PERCENTAGE, +) ATTR_BELT_UNIT_REMAINING_LIFE: Final = "belt_unit_remaining_life" ATTR_BLACK_DRUM_COUNTER: Final = "black_drum_counter" @@ -82,6 +86,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( key=ATTR_STATUS, icon="mdi:printer", name=ATTR_STATUS.title(), + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_PAGE_COUNTER, @@ -89,6 +94,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_PAGE_COUNTER.replace("_", " ").title(), native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_BW_COUNTER, @@ -96,6 +102,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_BW_COUNTER.replace("_", " ").title(), native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_COLOR_COUNTER, @@ -103,6 +110,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_COLOR_COUNTER.replace("_", " ").title(), native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_DUPLEX_COUNTER, @@ -110,6 +118,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_DRUM_REMAINING_LIFE, @@ -117,6 +126,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_BLACK_DRUM_REMAINING_LIFE, @@ -124,6 +134,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_CYAN_DRUM_REMAINING_LIFE, @@ -131,6 +142,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, @@ -138,6 +150,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_YELLOW_DRUM_REMAINING_LIFE, @@ -145,6 +158,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_BELT_UNIT_REMAINING_LIFE, @@ -152,6 +166,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_FUSER_REMAINING_LIFE, @@ -159,6 +174,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_LASER_REMAINING_LIFE, @@ -166,6 +182,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_PF_KIT_1_REMAINING_LIFE, @@ -173,6 +190,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_PF_KIT_MP_REMAINING_LIFE, @@ -180,6 +198,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_BLACK_TONER_REMAINING, @@ -187,6 +206,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_CYAN_TONER_REMAINING, @@ -194,6 +214,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_MAGENTA_TONER_REMAINING, @@ -201,6 +222,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_YELLOW_TONER_REMAINING, @@ -208,6 +230,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_BLACK_INK_REMAINING, @@ -215,6 +238,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_CYAN_INK_REMAINING, @@ -222,6 +246,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_MAGENTA_INK_REMAINING, @@ -229,6 +254,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_YELLOW_INK_REMAINING, @@ -236,11 +262,13 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_UPTIME, name=ATTR_UPTIME.title(), entity_registry_enabled_default=False, device_class=DEVICE_CLASS_TIMESTAMP, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 0365918a78b..77a84c70de8 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==1.0.2"], + "requirements": ["brother==1.1.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 8dd150b48bf..00cb91b2860 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -5,6 +5,7 @@ from typing import Any, cast from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -32,13 +33,14 @@ async def async_setup_entry( sensors = [] - device_info: DeviceInfo = { - "identifiers": {(DOMAIN, coordinator.data.serial)}, - "name": coordinator.data.model, - "manufacturer": ATTR_MANUFACTURER, - "model": coordinator.data.model, - "sw_version": getattr(coordinator.data, "firmware", None), - } + device_info = DeviceInfo( + configuration_url=f"http://{entry.data[CONF_HOST]}/", + identifiers={(DOMAIN, coordinator.data.serial)}, + manufacturer=ATTR_MANUFACTURER, + model=coordinator.data.model, + name=coordinator.data.model, + sw_version=getattr(coordinator.data, "firmware", None), + ) for description in SENSOR_TYPES: if description.key in coordinator.data: diff --git a/homeassistant/components/brother/translations/bg.json b/homeassistant/components/brother/translations/bg.json new file mode 100644 index 00000000000..afb3272e575 --- /dev/null +++ b/homeassistant/components/brother/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{model} {serial_number}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 5c9d7b3d4a5..650ce9c05c6 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -43,8 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): bapi = BruntAPI(username=username, password=password) try: - things = bapi.getThings()["things"] - if not things: + if not (things := bapi.getThings()["things"]): _LOGGER.error("No things present in account") else: add_entities( diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 23259101224..d7f4972c59e 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -20,16 +20,9 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_TEMPERATURE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_TARGET_TEMPERATURE, DATA_BSBLAN_CLIENT, DOMAIN @@ -98,12 +91,12 @@ class BSBLanClimate(ClimateEntity): self._store_hvac_mode = None self.bsblan = bsblan self._attr_name = self._attr_unique_id = info.device_identification - self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, info.device_identification)}, - ATTR_NAME: "BSBLan Device", - ATTR_MANUFACTURER: "BSBLan", - ATTR_MODEL: info.controller_variant, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, info.device_identification)}, + manufacturer="BSBLan", + model=info.controller_variant, + name="BSBLan Device", + ) async def async_set_preset_mode(self, preset_mode): """Set preset mode.""" diff --git a/homeassistant/components/bsblan/translations/bg.json b/homeassistant/components/bsblan/translations/bg.json new file mode 100644 index 00000000000..09a1668e142 --- /dev/null +++ b/homeassistant/components/bsblan/translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 91e4bcffb17..e68b096ca05 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -131,8 +131,7 @@ class BuienradarCam(Camera): _LOGGER.debug("HTTP 304 - success") return True - last_modified = res.headers.get("Last-Modified") - if last_modified: + if last_modified := res.headers.get("Last-Modified"): self._last_modified = last_modified self._last_image = await res.read() diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 2c6390f959b..0affe1e2c62 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -786,8 +786,7 @@ class BrSensor(SensorEntity): if sensor_type == SYMBOL or sensor_type.startswith(CONDITION): # update weather symbol & status text - condition = data.get(CONDITION) - if condition: + if condition := data.get(CONDITION): if sensor_type == SYMBOL: new_state = condition.get(EXACTNL) if sensor_type == CONDITION: diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 8934a7a6833..63c585f8c2f 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -1,6 +1,7 @@ """Shared utilities for different supported platforms.""" import asyncio from datetime import datetime, timedelta +from http import HTTPStatus import logging import aiohttp @@ -25,7 +26,7 @@ from buienradar.constants import ( ) from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, HTTP_OK +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -92,7 +93,7 @@ class BrData: result[STATUS_CODE] = resp.status result[CONTENT] = await resp.text() - if resp.status == HTTP_OK: + if resp.status == HTTPStatus.OK: result[SUCCESS] = True else: result[MESSAGE] = "Got http statuscode: %d" % (resp.status) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index a1546120064..aa336d3929c 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -133,12 +133,13 @@ class BrWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - if self._data and self._data.condition: - ccode = self._data.condition.get(CONDCODE) - if ccode: - conditions = self.hass.data[DOMAIN].get(DATA_CONDITION) - if conditions: - return conditions.get(ccode) + if ( + self._data + and self._data.condition + and (ccode := self._data.condition.get(CONDCODE)) + and (conditions := self.hass.data[DOMAIN].get(DATA_CONDITION)) + ): + return conditions.get(ccode) @property def temperature(self): diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 8809e05d25b..1666a30a1f5 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from http import HTTPStatus import logging import re from typing import cast, final @@ -10,7 +11,7 @@ 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.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -144,8 +145,7 @@ class CalendarEventDevice(Entity): @property def state_attributes(self): """Return the entity state attributes.""" - event = self.event - if event is None: + if (event := self.event) is None: return None event = normalize_event(event) @@ -161,8 +161,7 @@ class CalendarEventDevice(Entity): @property def state(self): """Return the state of the calendar event.""" - event = self.event - if event is None: + if (event := self.event) is None: return STATE_OFF event = normalize_event(event) @@ -200,12 +199,12 @@ class CalendarEventView(http.HomeAssistantView): start = request.query.get("start") end = request.query.get("end") if None in (start, end, entity): - return web.Response(status=HTTP_BAD_REQUEST) + return web.Response(status=HTTPStatus.BAD_REQUEST) try: start_date = dt.parse_datetime(start) end_date = dt.parse_datetime(end) except (ValueError, AttributeError): - return web.Response(status=HTTP_BAD_REQUEST) + return web.Response(status=HTTPStatus.BAD_REQUEST) event_list = await entity.async_get_events( request.app["hass"], start_date, end_date ) diff --git a/homeassistant/components/calendar/translations/ca.json b/homeassistant/components/calendar/translations/ca.json index f1b3279a4cb..63cffd7063f 100644 --- a/homeassistant/components/calendar/translations/ca.json +++ b/homeassistant/components/calendar/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Calendari" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index bfa68fe67e6..5a3d730e7d3 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -63,6 +63,8 @@ from .const import ( DATA_CAMERA_PREFS, DOMAIN, SERVICE_RECORD, + STREAM_TYPE_HLS, + STREAM_TYPE_WEB_RTC, ) from .img_util import scale_jpeg_camera_image from .prefs import CameraPreferences @@ -207,7 +209,6 @@ async def async_get_image( async def async_get_stream_source(hass: HomeAssistant, entity_id: str) -> str | None: """Fetch the stream source for a camera entity.""" camera = _get_camera_from_entity_id(hass, entity_id) - return await camera.stream_source() @@ -271,9 +272,7 @@ async def async_get_still_stream( def _get_camera_from_entity_id(hass: HomeAssistant, entity_id: str) -> Camera: """Get camera component from entity_id.""" - component = hass.data.get(DOMAIN) - - if component is None: + if (component := hass.data.get(DOMAIN)) is None: raise HomeAssistantError("Camera integration not set up") camera = component.get_entity(entity_id) @@ -303,6 +302,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail, SCHEMA_WS_CAMERA_THUMBNAIL ) hass.components.websocket_api.async_register_command(ws_camera_stream) + hass.components.websocket_api.async_register_command(ws_camera_web_rtc_offer) hass.components.websocket_api.async_register_command(websocket_get_prefs) hass.components.websocket_api.async_register_command(websocket_update_prefs) @@ -421,6 +421,18 @@ class Camera(Entity): """Return the interval between frames of the mjpeg stream.""" return MIN_STREAM_INTERVAL + @property + def frontend_stream_type(self) -> str | None: + """Return the type of stream supported by this camera. + + A camera may have a single stream type which is used to inform the + frontend which camera attributes and player to use. The default type + is to use HLS, and components can override to change the type. + """ + if not self.supported_features & SUPPORT_STREAM: + return None + return STREAM_TYPE_HLS + async def create_stream(self) -> Stream | None: """Create a Stream for stream_source.""" # There is at most one stream (a decode worker) per camera @@ -433,10 +445,20 @@ class Camera(Entity): return self.stream async def stream_source(self) -> str | None: - """Return the source of the stream.""" + """Return the source of the stream. + + This is used by cameras with SUPPORT_STREAM and STREAM_TYPE_HLS. + """ # pylint: disable=no-self-use return None + async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None: + """Handle the WebRTC offer and return an answer. + + This is used by cameras with SUPPORT_STREAM and STREAM_TYPE_WEB_RTC. + """ + raise NotImplementedError() + def camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: @@ -548,6 +570,11 @@ class Camera(Entity): if self.motion_detection_enabled: attrs["motion_detection"] = self.motion_detection_enabled + if self.frontend_stream_type: + attrs["frontend_stream_type"] = self.frontend_stream_type + # Remove after home-assistant/frontend#10298 is merged into nightly + attrs["stream_type"] = self.frontend_stream_type + return attrs @callback @@ -626,8 +653,7 @@ class CameraMjpegStream(CameraView): async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse: """Serve camera stream, possibly with interval.""" - interval_str = request.query.get("interval") - if interval_str is None: + if (interval_str := request.query.get("interval")) is None: stream = await camera.handle_async_mjpeg_stream(request) if stream is None: raise web.HTTPBadGateway() @@ -699,6 +725,50 @@ async def ws_camera_stream( ) +@websocket_api.websocket_command( + { + vol.Required("type"): "camera/web_rtc_offer", + vol.Required("entity_id"): cv.entity_id, + vol.Required("offer"): str, + } +) +@websocket_api.async_response +async def ws_camera_web_rtc_offer( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Handle the signal path for a WebRTC stream. + + This signal path is used to route the offer created by the client to the + camera device through the integration for negitioation on initial setup, + which returns an answer. The actual streaming is handled entirely between + the client and camera device. + + Async friendly. + """ + entity_id = msg["entity_id"] + offer = msg["offer"] + camera = _get_camera_from_entity_id(hass, entity_id) + if camera.frontend_stream_type != STREAM_TYPE_WEB_RTC: + connection.send_error( + msg["id"], + "web_rtc_offer_failed", + f"Camera does not support WebRTC, frontend_stream_type={camera.frontend_stream_type}", + ) + return + try: + answer = await camera.async_handle_web_rtc_offer(offer) + except (HomeAssistantError, ValueError) as ex: + _LOGGER.error("Error handling WebRTC offer: %s", ex) + connection.send_error(msg["id"], "web_rtc_offer_failed", str(ex)) + except asyncio.TimeoutError: + _LOGGER.error("Timeout handling WebRTC offer") + connection.send_error( + msg["id"], "web_rtc_offer_failed", "Timeout handling WebRTC offer" + ) + else: + connection.send_result(msg["id"], {"answer": answer}) + + @websocket_api.websocket_command( {vol.Required("type"): "camera/get_prefs", vol.Required("entity_id"): cv.entity_id} ) diff --git a/homeassistant/components/camera/const.py b/homeassistant/components/camera/const.py index 2cb01f44aa9..3eb131200e6 100644 --- a/homeassistant/components/camera/const.py +++ b/homeassistant/components/camera/const.py @@ -14,3 +14,12 @@ CONF_DURATION: Final = "duration" CAMERA_STREAM_SOURCE_TIMEOUT: Final = 10 CAMERA_IMAGE_TIMEOUT: Final = 10 + +# A camera that supports CAMERA_SUPPORT_STREAM may have a single stream +# type which is used to inform the frontend which player to use. +# Streams with RTSP sources typically use the stream component which uses +# HLS for display. WebRTC streams use the home assistant core for a signal +# path to initiate a stream, but the stream itself is between the client and +# device. +STREAM_TYPE_HLS = "hls" +STREAM_TYPE_WEB_RTC = "web_rtc" diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index a475a27f942..bbdaaf97d0d 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -20,6 +20,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import Throttle @@ -100,12 +101,12 @@ class CanaryCamera(CoordinatorEntity, Camera): self._live_stream_session: LiveStreamSession | None = None self._attr_name = device.name self._attr_unique_id = str(device.device_id) - self._attr_device_info = { - "identifiers": {(DOMAIN, str(device.device_id))}, - "name": device.name, - "model": device.device_type["name"], - "manufacturer": MANUFACTURER, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, str(device.device_id))}, + manufacturer=MANUFACTURER, + model=device.device_type["name"], + name=device.name, + ) @property def location(self) -> Location: diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 1e7747039b8..dbd94d98d58 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -114,12 +115,12 @@ class CanarySensor(CoordinatorEntity, SensorEntity): self._canary_type = canary_sensor_type self._attr_unique_id = f"{device.device_id}_{sensor_type[0]}" - self._attr_device_info = { - "identifiers": {(DOMAIN, str(device.device_id))}, - "name": device.name, - "model": device.device_type["name"], - "manufacturer": MANUFACTURER, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, str(device.device_id))}, + model=device.device_type["name"], + manufacturer=MANUFACTURER, + name=device.name, + ) self._attr_native_unit_of_measurement = sensor_type[1] self._attr_device_class = sensor_type[3] self._attr_icon = sensor_type[2] diff --git a/homeassistant/components/canary/translations/bg.json b/homeassistant/components/canary/translations/bg.json new file mode 100644 index 00000000000..337f1384446 --- /dev/null +++ b/homeassistant/components/canary/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 43b6b77ebd2..9ccac6e4f6c 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -18,9 +18,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass, config): """Set up the Cast component.""" - conf = config.get(DOMAIN) - - if conf is not None: + if (conf := config.get(DOMAIN)) is not None: media_player_config_validated = [] media_player_config = conf.get("media_player", {}) if not isinstance(media_player_config, list): diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 092d122d5cf..e74f0840a6c 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.2.1"], + "requirements": ["pychromecast==9.3.1"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 74c90f43372..eca7c69f5d2 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -55,6 +55,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.network import NoURLAvailableError, get_url import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_create_catching_coro @@ -184,12 +185,12 @@ class CastDevice(MediaPlayerEntity): self._attr_unique_id = cast_info.uuid self._attr_name = cast_info.friendly_name if cast_info.model_name != "Google Cast Group": - self._attr_device_info = { - "name": str(cast_info.friendly_name), - "identifiers": {(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, - "model": cast_info.model_name, - "manufacturer": str(cast_info.manufacturer), - } + self._attr_device_info = DeviceInfo( + identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, + manufacturer=str(cast_info.manufacturer), + model=cast_info.model_name, + name=str(cast_info.friendly_name), + ) async def async_added_to_hass(self): """Create chromecast object when added to hass.""" @@ -565,9 +566,7 @@ class CastDevice(MediaPlayerEntity): @property def state(self): """Return the state of the player.""" - media_status = self._media_status()[0] - - if media_status is None: + if (media_status := self._media_status()[0]) is None: return None if media_status.player_is_playing: return STATE_PLAYING @@ -588,8 +587,7 @@ class CastDevice(MediaPlayerEntity): @property def media_content_type(self): """Content type of current playing media.""" - media_status = self._media_status()[0] - if media_status is None: + if (media_status := self._media_status()[0]) is None: return None if media_status.media_is_tvshow: return MEDIA_TYPE_TVSHOW @@ -608,8 +606,7 @@ class CastDevice(MediaPlayerEntity): @property def media_image_url(self): """Image url of current playing media.""" - media_status = self._media_status()[0] - if media_status is None: + if (media_status := self._media_status()[0]) is None: return None images = media_status.images diff --git a/homeassistant/components/cert_expiry/translations/cs.json b/homeassistant/components/cert_expiry/translations/cs.json index c4b61df7084..44adaa19710 100644 --- a/homeassistant/components/cert_expiry/translations/cs.json +++ b/homeassistant/components/cert_expiry/translations/cs.json @@ -6,7 +6,8 @@ }, "error": { "connection_refused": "P\u0159ipojen\u00ed bylo odm\u00edtnuto p\u0159i p\u0159ipojov\u00e1n\u00ed k hostiteli", - "connection_timeout": "\u010casov\u00fd limit p\u0159i p\u0159ipojen\u00ed k tomuto hostiteli vypr\u0161el" + "connection_timeout": "\u010casov\u00fd limit p\u0159i p\u0159ipojen\u00ed k tomuto hostiteli vypr\u0161el", + "resolve_failed": "Tohoto hostitele nelze vy\u0159e\u0161it" }, "step": { "user": { @@ -14,7 +15,8 @@ "host": "Hostitel", "name": "N\u00e1zev certifik\u00e1tu", "port": "Port" - } + }, + "title": "Definujte certifik\u00e1t, kter\u00fd chcete testovat" } } }, diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py index 09096f44b74..fdefb25aef4 100644 --- a/homeassistant/components/clickatell/notify.py +++ b/homeassistant/components/clickatell/notify.py @@ -1,11 +1,12 @@ """Clickatell platform for notify component.""" +from http import HTTPStatus import logging import requests import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService -from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT, HTTP_ACCEPTED, HTTP_OK +from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -37,5 +38,5 @@ class ClickatellNotificationService(BaseNotificationService): data = {"apiKey": self.api_key, "to": self.recipient, "content": message} resp = requests.get(BASE_API_URL, params=data, timeout=5) - if (resp.status_code != HTTP_OK) or (resp.status_code != HTTP_ACCEPTED): + if resp.status_code not in (HTTPStatus.OK, HTTPStatus.ACCEPTED): _LOGGER.error("Error %s : %s", resp.status_code, resp.text) diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 18562260431..74f1c2e1ae5 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -1,4 +1,5 @@ """Clicksend platform for notify component.""" +from http import HTTPStatus import json import logging @@ -13,7 +14,6 @@ from homeassistant.const import ( CONF_SENDER, CONF_USERNAME, CONTENT_TYPE_JSON, - HTTP_OK, ) import homeassistant.helpers.config_validation as cv @@ -81,7 +81,7 @@ class ClicksendNotificationService(BaseNotificationService): auth=(self.username, self.api_key), timeout=TIMEOUT, ) - if resp.status_code == HTTP_OK: + if resp.status_code == HTTPStatus.OK: return obj = json.loads(resp.text) @@ -101,6 +101,4 @@ def _authenticate(config): auth=(config[CONF_USERNAME], config[CONF_API_KEY]), timeout=TIMEOUT, ) - if resp.status_code != HTTP_OK: - return False - return True + return resp.status_code == HTTPStatus.OK diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index 6648333bb54..712787c34e6 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -1,4 +1,5 @@ """clicksend_tts platform for notify component.""" +from http import HTTPStatus import json import logging @@ -12,7 +13,6 @@ from homeassistant.const import ( CONF_RECIPIENT, CONF_USERNAME, CONTENT_TYPE_JSON, - HTTP_OK, ) import homeassistant.helpers.config_validation as cv @@ -88,7 +88,7 @@ class ClicksendNotificationService(BaseNotificationService): timeout=TIMEOUT, ) - if resp.status_code == HTTP_OK: + if resp.status_code == HTTPStatus.OK: return obj = json.loads(resp.text) response_msg = obj["response_msg"] @@ -108,7 +108,4 @@ def _authenticate(config): timeout=TIMEOUT, ) - if resp.status_code != HTTP_OK: - return False - - return True + return resp.status_code == HTTPStatus.OK diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index 97090cef31d..c6a40b839f2 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -357,10 +357,10 @@ class ClimaCellEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return device registry information.""" - return { - "identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])}, - "name": "ClimaCell", - "manufacturer": "ClimaCell", - "sw_version": f"v{self.api_version}", - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self._config_entry.data[CONF_API_KEY])}, + manufacturer="ClimaCell", + name="ClimaCell", + sw_version=f"v{self.api_version}", + ) diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index 1ba5bbe3a34..f934449fdb0 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from abc import abstractmethod -import logging from pyclimacell.const import CURRENT @@ -21,8 +20,6 @@ from .const import ( ClimaCellSensorEntityDescription, ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -31,9 +28,8 @@ async def async_setup_entry( ) -> None: """Set up a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - api_version = config_entry.data[CONF_API_VERSION] - if api_version == 3: + if (api_version := config_entry.data[CONF_API_VERSION]) == 3: api_class = ClimaCellV3SensorEntity sensor_types = CC_V3_SENSOR_TYPES else: diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 865c2baa330..cb0783c6bee 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -4,7 +4,6 @@ from __future__ import annotations from abc import abstractmethod from collections.abc import Mapping from datetime import datetime -import logging from typing import Any from pyclimacell.const import ( @@ -94,8 +93,6 @@ from .const import ( MAX_FORECASTS, ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 55387d71438..773ee5920da 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -70,6 +70,7 @@ FAN_DIFFUSE = "diffuse" # Possible swing state +SWING_ON = "on" SWING_OFF = "off" SWING_BOTH = "both" SWING_VERTICAL = "vertical" diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index ce4e08f9fd2..05212e6ab99 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -118,9 +118,7 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - trigger_type = config[CONF_TYPE] - - if trigger_type == "hvac_mode_changed": + if (trigger_type := config[CONF_TYPE]) == "hvac_mode_changed": state_config = { state_trigger.CONF_PLATFORM: "state", state_trigger.CONF_ENTITY_ID: config[CONF_ENTITY_ID], diff --git a/homeassistant/components/climate/translations/ca.json b/homeassistant/components/climate/translations/ca.json index 3eb99744751..89720be754e 100644 --- a/homeassistant/components/climate/translations/ca.json +++ b/homeassistant/components/climate/translations/ca.json @@ -22,7 +22,7 @@ "fan_only": "Nom\u00e9s ventilador", "heat": "Escalfa", "heat_cool": "Escalfa/Refreda", - "off": "OFF" + "off": "off" } }, "title": "Climatitzaci\u00f3" diff --git a/homeassistant/components/climate/translations/hu.json b/homeassistant/components/climate/translations/hu.json index 400c1af877d..a31792fc016 100644 --- a/homeassistant/components/climate/translations/hu.json +++ b/homeassistant/components/climate/translations/hu.json @@ -2,11 +2,11 @@ "device_automation": { "action_type": { "set_hvac_mode": "F\u0171t\u00e9s- \u00e9s l\u00e9gtechnikai (HVAC) \u00fczemm\u00f3d m\u00f3dos\u00edt\u00e1sa a k\u00f6vetkez\u0151n: {entity_name}", - "set_preset_mode": "A(z) {entity_name} be\u00e1ll\u00edt\u00e1s\u00e1nak v\u00e1lt\u00e1sa" + "set_preset_mode": "{entity_name} \u00fczemm\u00f3dj\u00e1nak v\u00e1lt\u00e1sa" }, "condition_type": { "is_hvac_mode": "{entity_name} speci\u00e1lis f\u0171t\u00e9s, szell\u0151z\u00e9s \u00e9s l\u00e9gkondicion\u00e1l\u00e1s (HVAC) \u00fczemm\u00f3dra van be\u00e1ll\u00edtva", - "is_preset_mode": "A(z) {entity_name} el\u0151re be\u00e1ll\u00edtott m\u00f3dja van kiv\u00e1lasztva" + "is_preset_mode": "{entity_name} \u00fczemm\u00f3dja van kiv\u00e1lasztva" }, "trigger_type": { "current_humidity_changed": "{entity_name} m\u00e9rt p\u00e1ratartalma megv\u00e1ltozott", diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index f1833899fec..f99ac4c7b0a 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -253,9 +253,7 @@ def _remote_handle_prefs_updated(cloud: Cloud) -> None: if prefs.remote_enabled == cur_pref: return - cur_pref = prefs.remote_enabled - - if cur_pref: + if cur_pref := prefs.remote_enabled: await cloud.remote.connect() else: await cloud.remote.disconnect() diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 5bb0db6d057..6dc0da82512 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -41,9 +41,7 @@ async def async_provide_implementation(hass: HomeAssistant, domain: str): async def _get_services(hass): """Get the available services.""" - services = hass.data.get(DATA_SERVICES) - - if services is not None: + if (services := hass.data.get(DATA_SERVICES)) is not None: return services try: diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 43ef0ee62da..41bab5e0bd4 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -2,6 +2,7 @@ import asyncio from contextlib import suppress from datetime import timedelta +from http import HTTPStatus import logging import aiohttp @@ -15,9 +16,13 @@ from homeassistant.components.alexa import ( errors as alexa_errors, state_report as alexa_state_report, ) -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_BAD_REQUEST +from homeassistant.const import ( + CLOUD_NEVER_EXPOSED_ENTITIES, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, +) from homeassistant.core import HomeAssistant, callback, split_entity_id -from homeassistant.helpers import entity_registry, start +from homeassistant.helpers import entity_registry as er, start from homeassistant.helpers.event import async_call_later from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -110,7 +115,7 @@ class AlexaConfig(alexa_config.AbstractConfig): self._prefs.async_listen_updates(self._async_prefs_updated) self.hass.bus.async_listen( - entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entity_registry_updated, ) @@ -128,13 +133,20 @@ class AlexaConfig(alexa_config.AbstractConfig): if entity_expose is not None: return entity_expose - default_expose = self._prefs.alexa_default_expose + entity_registry = er.async_get(self.hass) + if registry_entry := entity_registry.async_get(entity_id): + auxiliary_entity = registry_entry.entity_category in ( + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ) + else: + auxiliary_entity = False # Backwards compat - if default_expose is None: - return True + if (default_expose := self._prefs.alexa_default_expose) is None: + return not auxiliary_entity - return split_entity_id(entity_id)[0] in default_expose + return not auxiliary_entity and split_entity_id(entity_id)[0] in default_expose @callback def async_invalidate_access_token(self): @@ -149,7 +161,7 @@ class AlexaConfig(alexa_config.AbstractConfig): resp = await cloud_api.async_alexa_access_token(self._cloud) body = await resp.json() - if resp.status == HTTP_BAD_REQUEST: + if resp.status == HTTPStatus.BAD_REQUEST: if body["reason"] in ("RefreshTokenNotFound", "UnknownRegion"): if self.should_report_state: await self._prefs.async_update(alexa_report_state=False) @@ -340,7 +352,7 @@ class AlexaConfig(alexa_config.AbstractConfig): elif action == "remove": to_remove.append(entity_id) elif action == "update" and bool( - set(event.data["changes"]) & entity_registry.ENTITY_DESCRIBING_ATTRIBUTES + set(event.data["changes"]) & er.ENTITY_DESCRIBING_ATTRIBUTES ): to_update.append(entity_id) if "old_entity_id" in event.data: diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index f96bda4ce1b..a27364c715f 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, BinarySensorEntity, ) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN @@ -28,6 +29,7 @@ class CloudRemoteBinary(BinarySensorEntity): _attr_device_class = DEVICE_CLASS_CONNECTIVITY _attr_should_poll = False _attr_unique_id = "cloud-remote-ui-connectivity" + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, cloud): """Initialize the binary sensor.""" diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 54c471e2a83..5a10e1d1e5c 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from http import HTTPStatus import logging from pathlib import Path from typing import Any @@ -14,7 +15,6 @@ from homeassistant.components.alexa import ( smart_home as alexa_sh, ) from homeassistant.components.google_assistant import const as gc, smart_home as ga -from homeassistant.const import HTTP_OK from homeassistant.core import Context, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later @@ -210,7 +210,7 @@ class CloudClient(Interface): break if found is None: - return {"status": HTTP_OK} + return {"status": HTTPStatus.OK} request = MockRequest( content=payload["body"].encode("utf-8"), diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index f1783771f2f..f3f5a64bbd6 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -1,5 +1,6 @@ """Google config for Cloud.""" import asyncio +from http import HTTPStatus import logging from hass_nabucasa import Cloud, cloud_api @@ -7,9 +8,13 @@ from hass_nabucasa.google_report_state import ErrorResponse from homeassistant.components.google_assistant.const import DOMAIN as GOOGLE_DOMAIN from homeassistant.components.google_assistant.helpers import AbstractConfig -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_OK +from homeassistant.const import ( + CLOUD_NEVER_EXPOSED_ENTITIES, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, +) from homeassistant.core import CoreState, split_entity_id -from homeassistant.helpers import entity_registry, start +from homeassistant.helpers import entity_registry as er, start from homeassistant.setup import async_setup_component from .const import ( @@ -104,7 +109,7 @@ class CloudGoogleConfig(AbstractConfig): self._prefs.async_listen_updates(self._async_prefs_updated) self.hass.bus.async_listen( - entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entity_registry_updated, ) @@ -126,13 +131,22 @@ class CloudGoogleConfig(AbstractConfig): if entity_expose is not None: return entity_expose + entity_registry = er.async_get(self.hass) + if registry_entry := entity_registry.async_get(entity_id): + auxiliary_entity = registry_entry.entity_category in ( + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ) + else: + auxiliary_entity = False + default_expose = self._prefs.google_default_expose # Backwards compat if default_expose is None: - return True + return not auxiliary_entity - return split_entity_id(entity_id)[0] in default_expose + return not auxiliary_entity and split_entity_id(entity_id)[0] in default_expose @property def agent_user_id(self): @@ -164,7 +178,7 @@ class CloudGoogleConfig(AbstractConfig): async def _async_request_sync_devices(self, agent_user_id: str): """Trigger a sync with Google.""" if self._sync_entities_lock.locked(): - return HTTP_OK + return HTTPStatus.OK async with self._sync_entities_lock: resp = await cloud_api.async_google_actions_request_sync(self._cloud) @@ -215,7 +229,7 @@ class CloudGoogleConfig(AbstractConfig): # Only consider entity registry updates if info relevant for Google has changed if event.data["action"] == "update" and not bool( - set(event.data["changes"]) & entity_registry.ENTITY_DESCRIBING_ATTRIBUTES + set(event.data["changes"]) & er.ENTITY_DESCRIBING_ATTRIBUTES ): return diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 1f17f46013e..5bfebec40a3 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -1,6 +1,7 @@ """The HTTP api to control the cloud integration.""" import asyncio from functools import wraps +from http import HTTPStatus import logging import aiohttp @@ -20,12 +21,6 @@ from homeassistant.components.google_assistant import helpers as google_helpers from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.websocket_api import const as ws_const -from homeassistant.const import ( - HTTP_BAD_GATEWAY, - HTTP_BAD_REQUEST, - HTTP_INTERNAL_SERVER_ERROR, - HTTP_UNAUTHORIZED, -) from .const import ( DOMAIN, @@ -48,19 +43,19 @@ _LOGGER = logging.getLogger(__name__) _CLOUD_ERRORS = { InvalidTrustedNetworks: ( - HTTP_INTERNAL_SERVER_ERROR, + HTTPStatus.INTERNAL_SERVER_ERROR, "Remote UI not compatible with 127.0.0.1/::1 as a trusted network.", ), InvalidTrustedProxies: ( - HTTP_INTERNAL_SERVER_ERROR, + HTTPStatus.INTERNAL_SERVER_ERROR, "Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.", ), asyncio.TimeoutError: ( - HTTP_BAD_GATEWAY, + HTTPStatus.BAD_GATEWAY, "Unable to reach the Home Assistant cloud.", ), aiohttp.ClientError: ( - HTTP_INTERNAL_SERVER_ERROR, + HTTPStatus.INTERNAL_SERVER_ERROR, "Error making internal request", ), } @@ -96,15 +91,15 @@ async def async_setup(hass): _CLOUD_ERRORS.update( { - auth.UserNotFound: (HTTP_BAD_REQUEST, "User does not exist."), - auth.UserNotConfirmed: (HTTP_BAD_REQUEST, "Email not confirmed."), + auth.UserNotFound: (HTTPStatus.BAD_REQUEST, "User does not exist."), + auth.UserNotConfirmed: (HTTPStatus.BAD_REQUEST, "Email not confirmed."), auth.UserExists: ( - HTTP_BAD_REQUEST, + HTTPStatus.BAD_REQUEST, "An account with the given email already exists.", ), - auth.Unauthenticated: (HTTP_UNAUTHORIZED, "Authentication failed."), + auth.Unauthenticated: (HTTPStatus.UNAUTHORIZED, "Authentication failed."), auth.PasswordChangeRequired: ( - HTTP_BAD_REQUEST, + HTTPStatus.BAD_REQUEST, "Password change required.", ), } @@ -157,7 +152,7 @@ def _process_cloud_exception(exc, where): if err_info is None: _LOGGER.exception("Unexpected error processing request for %s", where) - err_info = (HTTP_BAD_GATEWAY, f"Unexpected error: {exc}") + err_info = (HTTPStatus.BAD_GATEWAY, f"Unexpected error: {exc}") return err_info diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index c51d5278730..a4c81bcc64f 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -216,9 +216,7 @@ class CloudPreferences: @property def remote_enabled(self): """Return if remote is enabled on start.""" - enabled = self._prefs.get(PREF_ENABLE_REMOTE, False) - - if not enabled: + if not self._prefs.get(PREF_ENABLE_REMOTE, False): return False if self._has_local_trusted_network or self._has_local_trusted_proxies: @@ -307,9 +305,7 @@ class CloudPreferences: async def _load_cloud_user(self) -> User | None: """Load cloud user if available.""" - user_id = self._prefs.get(PREF_CLOUD_USER) - - if user_id is None: + if (user_id := self._prefs.get(PREF_CLOUD_USER)) is None: return None # Fetch the user. It can happen that the user no longer exists if diff --git a/homeassistant/components/cloud/translations/bg.json b/homeassistant/components/cloud/translations/bg.json new file mode 100644 index 00000000000..d6ab160fd29 --- /dev/null +++ b/homeassistant/components/cloud/translations/bg.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "remote_server": "\u041e\u0442\u0434\u0430\u043b\u0435\u0447\u0435\u043d \u0441\u044a\u0440\u0432\u044a\u0440", + "subscription_expiration": "\u0418\u0437\u0442\u0438\u0447\u0430\u043d\u0435 \u043d\u0430 \u0430\u0431\u043e\u043d\u0430\u043c\u0435\u043d\u0442\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 51c3e5f3a4e..00eacf7ca52 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -15,14 +15,10 @@ SUPPORT_LANGUAGES = list({key[0] for key in MAP_VOICE}) def validate_lang(value): """Validate chosen gender or language.""" - lang = value.get(CONF_LANG) - - if lang is None: + if (lang := value.get(CONF_LANG)) is None: return value - gender = value.get(CONF_GENDER) - - if gender is None: + if (gender := value.get(CONF_GENDER)) is None: gender = value[CONF_GENDER] = next( (chk_gender for chk_lang, chk_gender in MAP_VOICE if chk_lang == lang), None ) diff --git a/homeassistant/components/cloud/utils.py b/homeassistant/components/cloud/utils.py index 57f84b057f7..715a8119af9 100644 --- a/homeassistant/components/cloud/utils.py +++ b/homeassistant/components/cloud/utils.py @@ -8,9 +8,7 @@ from aiohttp import payload, web def aiohttp_serialize_response(response: web.Response) -> dict[str, Any]: """Serialize an aiohttp response to a dictionary.""" - body = response.body - - if body is None: + if (body := response.body) is None: pass elif isinstance(body, payload.StringPayload): # pylint: disable=protected-access diff --git a/homeassistant/components/cloudflare/translations/bg.json b/homeassistant/components/cloudflare/translations/bg.json new file mode 100644 index 00000000000..a34f51c1828 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/bg.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "invalid_zone": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0437\u043e\u043d\u0430" + }, + "flow_title": "{name}", + "step": { + "user": { + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043a\u044a\u043c Cloudflare" + }, + "zone": { + "data": { + "zone": "\u0417\u043e\u043d\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/ru.json b/homeassistant/components/cloudflare/translations/ru.json index 5b4eb7e67a3..d4dd9db4d33 100644 --- a/homeassistant/components/cloudflare/translations/ru.json +++ b/homeassistant/components/cloudflare/translations/ru.json @@ -15,7 +15,7 @@ "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." + "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": { diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index e3862d6347c..e7f94e4d603 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Co2signal integration.""" from __future__ import annotations -import logging from typing import Any import voluptuous as vol @@ -15,8 +14,6 @@ from . import APIRatelimitExceeded, CO2Error, InvalidAuth, UnknownError, get_dat from .const import CONF_COUNTRY_CODE, DOMAIN from .util import get_extra_name -_LOGGER = logging.getLogger(__name__) - TYPE_USE_HOME = "Use home location" TYPE_SPECIFY_COORDINATES = "Specify coordinates" TYPE_SPECIFY_COUNTRY = "Specify country code" diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index a4c1062e2c6..b7a36623a3c 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -15,15 +15,13 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_TOKEN, PERCENTAGE, ) from homeassistant.helpers import config_validation as cv, update_coordinator +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import StateType from . import CO2SignalCoordinator, CO2SignalResponse @@ -104,12 +102,13 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE "country_code": coordinator.data["countryCode"], ATTR_ATTRIBUTION: ATTRIBUTION, } - self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, coordinator.entry_id)}, - ATTR_NAME: "CO2 signal", - ATTR_MANUFACTURER: "Tmrow.com", - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + configuration_url="https://www.electricitymap.org/", + entry_type="service", + identifiers={(DOMAIN, coordinator.entry_id)}, + manufacturer="Tmrow.com", + name="CO2 signal", + ) self._attr_unique_id = ( f"{coordinator.entry_id}_{description.unique_id or description.key}" ) diff --git a/homeassistant/components/co2signal/translations/bg.json b/homeassistant/components/co2signal/translations/bg.json new file mode 100644 index 00000000000..bb253fb6e6b --- /dev/null +++ b/homeassistant/components/co2signal/translations/bg.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "coordinates": { + "data": { + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430" + } + }, + "country": { + "data": { + "country_code": "\u041a\u043e\u0434 \u043d\u0430 \u0434\u044a\u0440\u0436\u0430\u0432\u0430\u0442\u0430" + } + }, + "user": { + "data": { + "location": "\u041f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/zh-Hans.json b/homeassistant/components/co2signal/translations/zh-Hans.json index af750541de5..b883b58c215 100644 --- a/homeassistant/components/co2signal/translations/zh-Hans.json +++ b/homeassistant/components/co2signal/translations/zh-Hans.json @@ -1,10 +1,33 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", + "api_ratelimit": "API \u8c03\u7528\u9891\u7387\u8d85\u9650", + "unknown": "\u975e\u9884\u671f\u7684\u9519\u8bef" + }, + "error": { + "api_ratelimit": "API \u8c03\u7528\u9891\u7387\u8d85\u9650", + "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", + "unknown": "\u975e\u9884\u671f\u7684\u9519\u8bef" + }, "step": { + "coordinates": { + "data": { + "latitude": "\u7eac\u5ea6", + "longitude": "\u7ecf\u5ea6" + } + }, + "country": { + "data": { + "country_code": "\u56fd\u5bb6/\u5730\u533a\u4ee3\u7801" + } + }, "user": { "data": { - "api_key": "\u8bbf\u95ee\u4ee4\u724c" - } + "api_key": "\u8bbf\u95ee token", + "location": "\u83b7\u53d6\u6570\u636e\u7684\u4f4d\u7f6e" + }, + "description": "\u8bf7\u8bbf\u95ee https://co2signal.com/ \u6765\u83b7\u53d6 token\u3002" } } } diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 033c398e09c..111064924af 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index 486da82dfcd..65c2636cd82 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -192,9 +192,11 @@ WALLETS = { "PHP": "PHP", "PKR": "PKR", "PLN": "PLN", + "POLY": "POLY", "PYG": "PYG", "QAR": "QAR", "QNT": "QNT", + "RLY": "RLY", "REN": "REN", "REP": "REP", "REPV2": "REPV2", @@ -427,8 +429,10 @@ RATES = { "PHP": "PHP", "PKR": "PKR", "PLN": "PLN", + "POLY": "POLY", "PYG": "PYG", "QAR": "QAR", + "RLY": "RLY", "REN": "REN", "REP": "REP", "RON": "RON", diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index f37af04065e..b4ef4bb8e35 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -3,6 +3,7 @@ import logging from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.entity import DeviceInfo from .const import ( API_ACCOUNT_AMOUNT, @@ -104,6 +105,13 @@ class AccountSensor(SensorEntity): API_ACCOUNT_CURRENCY ] break + self._attr_device_info = DeviceInfo( + configuration_url="https://www.coinbase.com/settings/api", + entry_type="service", + identifiers={(DOMAIN, self._coinbase_data.user_id)}, + manufacturer="Coinbase.com", + name=f"Coinbase {self._coinbase_data.user_id[-4:]}", + ) @property def name(self): @@ -169,6 +177,13 @@ class ExchangeRateSensor(SensorEntity): 1 / float(self._coinbase_data.exchange_rates[API_RATES][self.currency]), 2 ) self._unit_of_measurement = exchange_base + self._attr_device_info = DeviceInfo( + configuration_url="https://www.coinbase.com/settings/api", + entry_type="service", + identifiers={(DOMAIN, self._coinbase_data.user_id)}, + manufacturer="Coinbase.com", + name=f"Coinbase {self._coinbase_data.user_id[-4:]}", + ) @property def name(self): diff --git a/homeassistant/components/coinbase/translations/bg.json b/homeassistant/components/coinbase/translations/bg.json new file mode 100644 index 00000000000..6888f4ddf35 --- /dev/null +++ b/homeassistant/components/coinbase/translations/bg.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } + } + }, + "options": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/cs.json b/homeassistant/components/coinbase/translations/cs.json index c6f6a1f36f9..d25e431651d 100644 --- a/homeassistant/components/coinbase/translations/cs.json +++ b/homeassistant/components/coinbase/translations/cs.json @@ -11,8 +11,11 @@ "step": { "user": { "data": { - "api_key": "Kl\u00ed\u010d API" - } + "api_key": "Kl\u00ed\u010d API", + "api_token": "API Secret", + "exchange_rates": "Sm\u011bnn\u00e9 kurzy" + }, + "title": "Podrobnosti o API kl\u00ed\u010di Coinbase" } } }, diff --git a/homeassistant/components/coinbase/translations/zh-Hant.json b/homeassistant/components/coinbase/translations/zh-Hant.json index 5db1da7d23b..e6c92f1cb75 100644 --- a/homeassistant/components/coinbase/translations/zh-Hant.json +++ b/homeassistant/components/coinbase/translations/zh-Hant.json @@ -31,7 +31,7 @@ "init": { "data": { "account_balance_currencies": "\u5e33\u6236\u9918\u984d\u56de\u5831\u503c\u3002", - "exchange_base": "\u532f\u7387\u50b3\u611f\u5668\u57fa\u6e96\u8ca8\u5e63\u3002", + "exchange_base": "\u532f\u7387\u611f\u6e2c\u5668\u57fa\u6e96\u8ca8\u5e63\u3002", "exchange_rate_currencies": "\u532f\u7387\u56de\u5831\u503c\u3002" }, "description": "\u8abf\u6574 Coinbase \u9078\u9805" diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 257c6b4a354..110326bc1b2 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -130,8 +130,7 @@ class CompensationSensor(SensorEntity): @callback def _async_compensation_sensor_state_listener(self, event): """Handle sensor state changes.""" - new_state = event.data.get("new_state") - if new_state is None: + if (new_state := event.data.get("new_state")) is None: return if self._unit_of_measurement is None and self._source_attribute is None: diff --git a/homeassistant/components/config/area_registry.py b/homeassistant/components/config/area_registry.py index f40ed7834e3..09cd1c1c8ce 100644 --- a/homeassistant/components/config/area_registry.py +++ b/homeassistant/components/config/area_registry.py @@ -2,124 +2,102 @@ import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.components.websocket_api.decorators import ( - async_response, - require_admin, -) from homeassistant.core import callback -from homeassistant.helpers.area_registry import async_get_registry - -WS_TYPE_LIST = "config/area_registry/list" -SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_LIST} -) - -WS_TYPE_CREATE = "config/area_registry/create" -SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_CREATE, vol.Required("name"): str} -) - -WS_TYPE_DELETE = "config/area_registry/delete" -SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_DELETE, vol.Required("area_id"): str} -) - -WS_TYPE_UPDATE = "config/area_registry/update" -SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_UPDATE, - vol.Required("area_id"): str, - vol.Required("name"): str, - } -) +from homeassistant.helpers.area_registry import async_get async def async_setup(hass): """Enable the Area Registry views.""" - hass.components.websocket_api.async_register_command( - WS_TYPE_LIST, websocket_list_areas, SCHEMA_WS_LIST - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_CREATE, websocket_create_area, SCHEMA_WS_CREATE - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_DELETE, websocket_delete_area, SCHEMA_WS_DELETE - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_UPDATE, websocket_update_area, SCHEMA_WS_UPDATE - ) + hass.components.websocket_api.async_register_command(websocket_list_areas) + hass.components.websocket_api.async_register_command(websocket_create_area) + hass.components.websocket_api.async_register_command(websocket_delete_area) + hass.components.websocket_api.async_register_command(websocket_update_area) return True -@async_response -async def websocket_list_areas(hass, connection, msg): +@websocket_api.websocket_command({vol.Required("type"): "config/area_registry/list"}) +@callback +def websocket_list_areas(hass, connection, msg): """Handle list areas command.""" - registry = await async_get_registry(hass) - connection.send_message( - websocket_api.result_message( - msg["id"], - [ - {"name": entry.name, "area_id": entry.id} - for entry in registry.async_list_areas() - ], - ) + registry = async_get(hass) + connection.send_result( + msg["id"], + [_entry_dict(entry) for entry in registry.async_list_areas()], ) -@require_admin -@async_response -async def websocket_create_area(hass, connection, msg): +@websocket_api.websocket_command( + { + vol.Required("type"): "config/area_registry/create", + vol.Required("name"): str, + vol.Optional("picture"): vol.Any(str, None), + } +) +@websocket_api.require_admin +@callback +def websocket_create_area(hass, connection, msg): """Create area command.""" - registry = await async_get_registry(hass) + registry = async_get(hass) + + data = dict(msg) + data.pop("type") + data.pop("id") + try: - entry = registry.async_create(msg["name"]) + entry = registry.async_create(**data) except ValueError as err: - connection.send_message( - websocket_api.error_message(msg["id"], "invalid_info", str(err)) - ) + connection.send_error(msg["id"], "invalid_info", str(err)) else: - connection.send_message( - websocket_api.result_message(msg["id"], _entry_dict(entry)) - ) + connection.send_result(msg["id"], _entry_dict(entry)) -@require_admin -@async_response -async def websocket_delete_area(hass, connection, msg): +@websocket_api.websocket_command( + { + vol.Required("type"): "config/area_registry/delete", + vol.Required("area_id"): str, + } +) +@websocket_api.require_admin +@callback +def websocket_delete_area(hass, connection, msg): """Delete area command.""" - registry = await async_get_registry(hass) + registry = async_get(hass) try: registry.async_delete(msg["area_id"]) except KeyError: - connection.send_message( - websocket_api.error_message( - msg["id"], "invalid_info", "Area ID doesn't exist" - ) - ) + connection.send_error(msg["id"], "invalid_info", "Area ID doesn't exist") else: connection.send_message(websocket_api.result_message(msg["id"], "success")) -@require_admin -@async_response -async def websocket_update_area(hass, connection, msg): +@websocket_api.websocket_command( + { + vol.Required("type"): "config/area_registry/update", + vol.Required("area_id"): str, + vol.Optional("name"): str, + vol.Optional("picture"): vol.Any(str, None), + } +) +@websocket_api.require_admin +@callback +def websocket_update_area(hass, connection, msg): """Handle update area websocket command.""" - registry = await async_get_registry(hass) + registry = async_get(hass) + + data = dict(msg) + data.pop("type") + data.pop("id") try: - entry = registry.async_update(msg["area_id"], msg["name"]) + entry = registry.async_update(**data) except ValueError as err: - connection.send_message( - websocket_api.error_message(msg["id"], "invalid_info", str(err)) - ) + connection.send_error(msg["id"], "invalid_info", str(err)) else: - connection.send_message( - websocket_api.result_message(msg["id"], _entry_dict(entry)) - ) + connection.send_result(msg["id"], _entry_dict(entry)) @callback def _entry_dict(entry): """Convert entry to API format.""" - return {"area_id": entry.id, "name": entry.name} + return {"area_id": entry.id, "name": entry.name, "picture": entry.picture} diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index a8421c4c0f6..78175678a58 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -103,8 +103,7 @@ async def websocket_delete(hass, connection, msg): @websocket_api.async_response async def websocket_change_password(hass, connection, msg): """Change current user password.""" - user = connection.user - if user is None: + if (user := connection.user) is None: connection.send_error(msg["id"], "user_not_found", "User not found") return diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 4363fbbbe4d..1cc63297352 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -67,17 +67,18 @@ async def websocket_update_device(hass, connection, msg): def _entry_dict(entry): """Convert entry to API format.""" return { + "area_id": entry.area_id, + "configuration_url": entry.configuration_url, "config_entries": list(entry.config_entries), "connections": list(entry.connections), - "manufacturer": entry.manufacturer, - "model": entry.model, - "name": entry.name, - "sw_version": entry.sw_version, + "disabled_by": entry.disabled_by, "entry_type": entry.entry_type, "id": entry.id, "identifiers": list(entry.identifiers), - "via_device_id": entry.via_device_id, - "area_id": entry.area_id, + "manufacturer": entry.manufacturer, + "model": entry.model, "name_by_user": entry.name_by_user, - "disabled_by": entry.disabled_by, + "name": entry.name, + "sw_version": entry.sw_version, + "via_device_id": entry.via_device_id, } diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 43196acf319..9b6fc2af82a 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -50,9 +50,8 @@ async def websocket_get_entity(hass, connection, msg): Async friendly. """ registry = await async_get_registry(hass) - entry = registry.entities.get(msg["entity_id"]) - if entry is None: + if (entry := registry.entities.get(msg["entity_id"])) is None: connection.send_message( websocket_api.error_message(msg["id"], ERR_NOT_FOUND, "Entity not found") ) @@ -177,6 +176,7 @@ def _entry_dict(entry): "name": entry.name, "icon": entry.icon, "platform": entry.platform, + "entity_category": entry.entity_category, } diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index f8a3ac0cd9f..63b7bdf9868 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -7,7 +7,6 @@ from aiohttp.web import Response from homeassistant.components.http import HomeAssistantView from homeassistant.components.zwave import DEVICE_CONFIG_SCHEMA_ENTRY, const -from homeassistant.const import HTTP_BAD_REQUEST import homeassistant.core as ha import homeassistant.helpers.config_validation as cv @@ -52,7 +51,7 @@ class ZWaveLogView(HomeAssistantView): try: lines = int(request.query.get("lines", 0)) except ValueError: - return Response(text="Invalid datetime", status=HTTP_BAD_REQUEST) + return Response(text="Invalid datetime", status=HTTPStatus.BAD_REQUEST) hass = request.app["hass"] response = await hass.async_add_executor_job(self._get_log, hass, lines) @@ -81,8 +80,7 @@ class ZWaveConfigWriteView(HomeAssistantView): def post(self, request): """Save cache configuration to zwcfg_xxxxx.xml.""" hass = request.app["hass"] - network = hass.data.get(const.DATA_NETWORK) - if network is None: + if (network := hass.data.get(const.DATA_NETWORK)) is None: return self.json_message( "No Z-Wave network data found", HTTPStatus.NOT_FOUND ) @@ -132,8 +130,7 @@ class ZWaveNodeGroupView(HomeAssistantView): nodeid = int(node_id) hass = request.app["hass"] network = hass.data.get(const.DATA_NETWORK) - node = network.nodes.get(nodeid) - if node is None: + if (node := network.nodes.get(nodeid)) is None: return self.json_message("Node not found", HTTPStatus.NOT_FOUND) groupdata = node.groups groups = {} @@ -159,8 +156,7 @@ class ZWaveNodeConfigView(HomeAssistantView): nodeid = int(node_id) hass = request.app["hass"] network = hass.data.get(const.DATA_NETWORK) - node = network.nodes.get(nodeid) - if node is None: + if (node := network.nodes.get(nodeid)) is None: return self.json_message("Node not found", HTTPStatus.NOT_FOUND) config = {} for value in node.get_values( @@ -190,8 +186,7 @@ class ZWaveUserCodeView(HomeAssistantView): nodeid = int(node_id) hass = request.app["hass"] network = hass.data.get(const.DATA_NETWORK) - node = network.nodes.get(nodeid) - if node is None: + if (node := network.nodes.get(nodeid)) is None: return self.json_message("Node not found", HTTPStatus.NOT_FOUND) usercodes = {} if not node.has_command_class(const.COMMAND_CLASS_USER_CODE): @@ -221,8 +216,7 @@ class ZWaveProtectionView(HomeAssistantView): def _fetch_protection(): """Get protection data.""" - node = network.nodes.get(nodeid) - if node is None: + if (node := network.nodes.get(nodeid)) is None: return self.json_message("Node not found", HTTPStatus.NOT_FOUND) protection_options = {} if not node.has_command_class(const.COMMAND_CLASS_PROTECTION): diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index f06ec330815..fde0cdc590d 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -65,9 +65,7 @@ def async_request_config( if description_image is not None: description += f"\n\n![Description image]({description_image})" - instance = hass.data.get(_KEY_INSTANCE) - - if instance is None: + if (instance := hass.data.get(_KEY_INSTANCE)) is None: instance = hass.data[_KEY_INSTANCE] = Configurator(hass) request_id = instance.async_request_config( diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index b7806e665f3..e57abfa3b73 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, device_registry as dr +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -119,7 +120,7 @@ async def update_listener(hass, config_entry): await hass.config_entries.async_reload(config_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.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -147,7 +148,6 @@ class Control4Entity(CoordinatorEntity): def __init__( self, entry_data: dict, - entry: ConfigEntry, coordinator: DataUpdateCoordinator, name: str, idx: int, @@ -158,9 +158,9 @@ class Control4Entity(CoordinatorEntity): ) -> None: """Initialize a Control4 entity.""" super().__init__(coordinator) - self.entry = entry self.entry_data = entry_data - self._name = name + self._attr_name = name + self._attr_unique_id = str(idx) self._idx = idx self._controller_unique_id = entry_data[CONF_CONTROLLER_UNIQUE_ID] self._device_name = device_name @@ -169,23 +169,12 @@ class Control4Entity(CoordinatorEntity): self._device_id = device_id @property - def name(self): - """Return name of entity.""" - return self._name - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return str(self._idx) - - @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return info of parent Control4 device of entity.""" - return { - "config_entry_id": self.entry.entry_id, - "identifiers": {(DOMAIN, self._device_id)}, - "name": self._device_name, - "manufacturer": self._device_manufacturer, - "model": self._device_model, - "via_device": (DOMAIN, self._controller_unique_id), - } + return DeviceInfo( + identifiers={(DOMAIN, str(self._device_id))}, + manufacturer=self._device_manufacturer, + model=self._device_model, + name=self._device_name, + via_device=(DOMAIN, self._controller_unique_id), + ) diff --git a/homeassistant/components/control4/light.py b/homeassistant/components/control4/light.py index 38eca233f27..b2e5f6b43cf 100644 --- a/homeassistant/components/control4/light.py +++ b/homeassistant/components/control4/light.py @@ -122,7 +122,6 @@ async def async_setup_entry( entity_list.append( Control4Light( entry_data, - entry, item_coordinator, item_name, item_id, @@ -143,7 +142,6 @@ class Control4Light(Control4Entity, LightEntity): def __init__( self, entry_data: dict, - entry: ConfigEntry, coordinator: DataUpdateCoordinator, name: str, idx: int, @@ -156,7 +154,6 @@ class Control4Light(Control4Entity, LightEntity): """Initialize Control4 light entity.""" super().__init__( entry_data, - entry, coordinator, name, idx, diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 4d3297d8c65..401d240957e 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -154,8 +154,7 @@ class ConversationProcessView(http.HomeAssistantView): async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: """Get the active conversation agent.""" - agent = hass.data.get(DATA_AGENT) - if agent is None: + if (agent := hass.data.get(DATA_AGENT)) is None: agent = hass.data[DATA_AGENT] = DefaultAgent(hass) await agent.async_initialize(hass.data.get(DATA_CONFIG)) return agent diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 1773ca46cb5..d957eb8e0b2 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -66,9 +66,7 @@ class DefaultAgent(AbstractConversationAgent): intents = self.hass.data.setdefault(DOMAIN, {}) for intent_type, utterances in config.get("intents", {}).items(): - conf = intents.get(intent_type) - - if conf is None: + if (conf := intents.get(intent_type)) is None: conf = intents[intent_type] = [] conf.extend(create_matcher(utterance) for utterance in utterances) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 7077854a768..015a68ae18e 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -15,6 +15,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_SUPPORTED_MODES, DATA_COORDINATOR, DATA_INFO, DOMAIN @@ -73,15 +74,15 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): super()._handle_coordinator_update() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info for this device.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "CoolAutomation", - "model": "CoolMasterNet", - "sw_version": self._info["version"], - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="CoolAutomation", + model="CoolMasterNet", + name=self.name, + sw_version=self._info["version"], + ) @property def unique_id(self): @@ -120,8 +121,7 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): def hvac_mode(self): """Return hvac target hvac state.""" mode = self._unit.mode - is_on = self._unit.is_on - if not is_on: + if not self._unit.is_on: return HVAC_MODE_OFF return CM_TO_HA_STATE[mode] @@ -143,8 +143,7 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): async def async_set_temperature(self, **kwargs): """Set new target temperatures.""" - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is not None: + if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: _LOGGER.debug("Setting temp of %s to %s", self.unique_id, str(temp)) self._unit = await self._unit.set_thermostat(temp) self.async_write_ha_state() diff --git a/homeassistant/components/coolmaster/translations/bg.json b/homeassistant/components/coolmaster/translations/bg.json index 079082c01cf..72b8df6634d 100644 --- a/homeassistant/components/coolmaster/translations/bg.json +++ b/homeassistant/components/coolmaster/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "no_units": "\u041d\u0435 \u0431\u044f\u0445\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u0447\u043d\u0438/\u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u043d\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u0438\u044f CoolMasterNet \u0430\u0434\u0440\u0435\u0441." }, "step": { diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py index 0ced9bad06d..2029321c430 100644 --- a/homeassistant/components/counter/reproduce_state.py +++ b/homeassistant/components/counter/reproduce_state.py @@ -30,9 +30,7 @@ async def _async_reproduce_state( 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: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 00fef5c6485..7a3061d24c8 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -212,9 +212,7 @@ class CoverEntity(Entity): if self.is_closing: return STATE_CLOSING - closed = self.is_closed - - if closed is None: + if (closed := self.is_closed) is None: return None return STATE_CLOSED if closed else STATE_OPEN @@ -225,13 +223,11 @@ class CoverEntity(Entity): """Return the state attributes.""" data = {} - current = self.current_cover_position - if current is not None: - data[ATTR_CURRENT_POSITION] = self.current_cover_position + if (current := self.current_cover_position) is not None: + data[ATTR_CURRENT_POSITION] = current - current_tilt = self.current_cover_tilt_position - if current_tilt is not None: - data[ATTR_CURRENT_TILT_POSITION] = self.current_cover_tilt_position + if (current_tilt := self.current_cover_tilt_position) is not None: + data[ATTR_CURRENT_TILT_POSITION] = current_tilt return data diff --git a/homeassistant/components/cover/reproduce_state.py b/homeassistant/components/cover/reproduce_state.py index c96b9ec5acc..1be68bcfeba 100644 --- a/homeassistant/components/cover/reproduce_state.py +++ b/homeassistant/components/cover/reproduce_state.py @@ -42,9 +42,7 @@ async def _async_reproduce_state( 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: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/cover/translations/he.json b/homeassistant/components/cover/translations/he.json index 5dad593467c..66f6b9f3bbe 100644 --- a/homeassistant/components/cover/translations/he.json +++ b/homeassistant/components/cover/translations/he.json @@ -1,7 +1,29 @@ { "device_automation": { "action_type": { + "close": "\u05e1\u05d2\u05d9\u05e8\u05ea {entity_name}", + "close_tilt": "\u05e1\u05d2\u05d9\u05e8\u05ea \u05d4\u05d8\u05d9\u05d4 \u05e9\u05dc {entity_name}", + "open": "\u05e4\u05ea\u05d9\u05d7\u05ea {entity_name}", + "open_tilt": "\u05e4\u05ea\u05d9\u05d7\u05ea \u05d4\u05d8\u05d9\u05d9\u05ea {entity_name}", + "set_position": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05de\u05d9\u05e7\u05d5\u05dd {entity_name}", + "set_tilt_position": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05d8\u05d9\u05d9\u05ea {entity_name}", "stop": "\u05e2\u05e6\u05d5\u05e8 {entity_name}" + }, + "condition_type": { + "is_closed": "{entity_name} \u05e1\u05d2\u05d5\u05e8", + "is_closing": "{entity_name} \u05e0\u05e1\u05d2\u05e8", + "is_open": "{entity_name} \u05e4\u05ea\u05d5\u05d7", + "is_opening": "{entity_name} \u05e0\u05e4\u05ea\u05d7", + "is_position": "\u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05e0\u05d5\u05db\u05d7\u05d9 {entity_name} \u05d4\u05d5\u05d0", + "is_tilt_position": "\u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05d4\u05d8\u05d9\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9 {entity_name} \u05d4\u05d5\u05d0" + }, + "trigger_type": { + "closed": "{entity_name} \u05e1\u05d2\u05d5\u05e8", + "closing": "{entity_name} \u05e0\u05e1\u05d2\u05e8", + "opened": "{entity_name} \u05e0\u05e4\u05ea\u05d7", + "opening": "\u05e4\u05ea\u05d9\u05d7\u05ea {entity_name}", + "position": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05de\u05d9\u05e7\u05d5\u05dd", + "tilt_position": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05d8\u05d9\u05d4" } }, "state": { diff --git a/homeassistant/components/crownstone/devices.py b/homeassistant/components/crownstone/devices.py index 91af18ab15e..ead2c54a58e 100644 --- a/homeassistant/components/crownstone/devices.py +++ b/homeassistant/components/crownstone/devices.py @@ -3,13 +3,6 @@ from __future__ import annotations from crownstone_cloud.cloud_models.crownstones import Crownstone -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, -) from homeassistant.helpers.entity import DeviceInfo, Entity from .const import CROWNSTONE_INCLUDE_TYPES, DOMAIN @@ -36,10 +29,10 @@ class CrownstoneBaseEntity(Entity): @property def device_info(self) -> DeviceInfo: """Return device info.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self.cloud_id)}, - ATTR_NAME: self.device.name, - ATTR_MANUFACTURER: "Crownstone", - ATTR_MODEL: CROWNSTONE_INCLUDE_TYPES[self.device.type], - ATTR_SW_VERSION: self.device.sw_version, - } + return DeviceInfo( + identifiers={(DOMAIN, self.cloud_id)}, + manufacturer="Crownstone", + model=CROWNSTONE_INCLUDE_TYPES[self.device.type], + name=self.device.name, + sw_version=self.device.sw_version, + ) diff --git a/homeassistant/components/crownstone/translations/bg.json b/homeassistant/components/crownstone/translations/bg.json new file mode 100644 index 00000000000..2c567e2a1e8 --- /dev/null +++ b/homeassistant/components/crownstone/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/cs.json b/homeassistant/components/crownstone/translations/cs.json index a7aaa1746f9..f1e209b21d8 100644 --- a/homeassistant/components/crownstone/translations/cs.json +++ b/homeassistant/components/crownstone/translations/cs.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, "step": { "usb_manual_config": { "data": { @@ -16,11 +22,21 @@ }, "options": { "step": { + "usb_config": { + "data": { + "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" + } + }, "usb_config_option": { "data": { "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" } }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Cesta k USB za\u0159\u00edzen\u00ed" + } + }, "usb_manual_config_option": { "data": { "usb_manual_path": "Cesta k USB za\u0159\u00edzen\u00ed" diff --git a/homeassistant/components/crownstone/translations/pl.json b/homeassistant/components/crownstone/translations/pl.json new file mode 100644 index 00000000000..c71c27c4601 --- /dev/null +++ b/homeassistant/components/crownstone/translations/pl.json @@ -0,0 +1,54 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "\u015acie\u017cka urz\u0105dzenia USB" + } + }, + "user": { + "data": { + "email": "E-mail", + "password": "Has\u0142o" + }, + "title": "Konto Crownstone" + } + } + }, + "options": { + "step": { + "usb_config": { + "data": { + "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" + } + }, + "usb_config_option": { + "data": { + "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "\u015acie\u017cka urz\u0105dzenia USB" + } + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "\u015acie\u017cka urz\u0105dzenia USB" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index fd3f3b2f8c5..2f0461cc8c4 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -92,8 +92,7 @@ class CurrencylayerSensor(SensorEntity): def update(self): """Update current date.""" self.rest.update() - value = self.rest.data - if value is not None: + if (value := self.rest.data) is not None: self._state = round(value[f"{self._base}{self._quote}"], 4) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 504f8cd9f86..185537cc7d0 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import Throttle from .const import CONF_UUID, DOMAIN, KEY_MAC, TIMEOUT @@ -109,13 +110,13 @@ class DaikinApi: return self._available @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" info = self.device.values - return { - "connections": {(CONNECTION_NETWORK_MAC, self.device.mac)}, - "manufacturer": "Daikin", - "model": info.get("model"), - "name": info.get("name"), - "sw_version": info.get("ver", "").replace("_", "."), - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self.device.mac)}, + manufacturer="Daikin", + model=info.get("model"), + name=info.get("name"), + sw_version=info.get("ver", "").replace("_", "."), + ) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index b3e833bb64f..c8e962b9c76 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -136,12 +136,10 @@ class DaikinClimate(ClimateEntity): values = {} for attr in (ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE, ATTR_HVAC_MODE): - value = settings.get(attr) - if value is None: + if (value := settings.get(attr)) is None: continue - daikin_attr = HA_ATTR_TO_DAIKIN.get(attr) - if daikin_attr is not None: + if (daikin_attr := HA_ATTR_TO_DAIKIN.get(attr)) is not None: if attr == ATTR_HVAC_MODE: values[daikin_attr] = HA_STATE_TO_DAIKIN[value] elif value in self._list[attr]: diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 43d169e3440..18447c56d18 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -5,7 +5,7 @@ from uuid import uuid4 from aiohttp import ClientError, web_exceptions from async_timeout import timeout -from pydaikin.daikin_base import Appliance +from pydaikin.daikin_base import Appliance, DaikinException from pydaikin.discovery import Discovery import voluptuous as vol @@ -88,6 +88,13 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=self.schema, errors={"base": "invalid_auth"}, ) + except DaikinException as daikin_exp: + _LOGGER.error(daikin_exp) + return self.async_show_form( + step_id="user", + data_schema=self.schema, + errors={"base": "unknown"}, + ) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error creating device") return self.async_show_form( diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index f34dc8edc57..2a1619594ba 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.4"], + "requirements": ["pydaikin==2.6.0"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum", diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 5e0e1b5761a..647ee0689e6 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -22,8 +22,7 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up Daikin climate based on config_entry.""" daikin_api = hass.data[DAIKIN_DOMAIN][entry.entry_id] switches = [] - zones = daikin_api.device.zones - if zones: + if zones := daikin_api.device.zones: switches.extend( [ DaikinZoneSwitch(daikin_api, zone_id) diff --git a/homeassistant/components/daikin/translations/bg.json b/homeassistant/components/daikin/translations/bg.json index a1f8209b2bf..5a8e7d875f9 100644 --- a/homeassistant/components/daikin/translations/bg.json +++ b/homeassistant/components/daikin/translations/bg.json @@ -1,12 +1,17 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "api_password": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435, \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 API \u043a\u043b\u044e\u0447 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430." }, "step": { "user": { "data": { - "host": "\u0410\u0434\u0440\u0435\u0441" + "host": "\u0410\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u043a Daikin.", "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u043a Daikin" diff --git a/homeassistant/components/daikin/translations/ca.json b/homeassistant/components/daikin/translations/ca.json index 0b8c2aae7eb..a895697f490 100644 --- a/homeassistant/components/daikin/translations/ca.json +++ b/homeassistant/components/daikin/translations/ca.json @@ -5,6 +5,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3" }, "error": { + "api_password": "Autenticaci\u00f3 inv\u00e0lida, utilitza la clau API o la contrasenya.", "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" diff --git a/homeassistant/components/daikin/translations/de.json b/homeassistant/components/daikin/translations/de.json index 038a997201a..3310d96da60 100644 --- a/homeassistant/components/daikin/translations/de.json +++ b/homeassistant/components/daikin/translations/de.json @@ -5,6 +5,7 @@ "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { + "api_password": "Ung\u00fcltige Authentifizierung, verwende entweder den API-Schl\u00fcssel oder das Passwort.", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/daikin/translations/et.json b/homeassistant/components/daikin/translations/et.json index 0f303d2014e..9fa1ea3b293 100644 --- a/homeassistant/components/daikin/translations/et.json +++ b/homeassistant/components/daikin/translations/et.json @@ -5,6 +5,7 @@ "cannot_connect": "\u00dchendamine nurjus" }, "error": { + "api_password": "Vigane autentimine , kasuta kas API v\u00f5tit v\u00f5i salas\u00f5na.", "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamise viga", "unknown": "Tundmatu viga" diff --git a/homeassistant/components/daikin/translations/hu.json b/homeassistant/components/daikin/translations/hu.json index 6049890cb53..f5f774e1527 100644 --- a/homeassistant/components/daikin/translations/hu.json +++ b/homeassistant/components/daikin/translations/hu.json @@ -5,6 +5,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { + "api_password": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s, haszn\u00e1ljon API-kulcsot vagy jelsz\u00f3t.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/daikin/translations/is.json b/homeassistant/components/daikin/translations/is.json new file mode 100644 index 00000000000..c0d8b4164da --- /dev/null +++ b/homeassistant/components/daikin/translations/is.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "api_password": "\u00d3gild au\u00f0kenning, nota\u00f0u anna\u00f0hvort API lykil e\u00f0a lykilor\u00f0." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/it.json b/homeassistant/components/daikin/translations/it.json index c85f4961e57..203335c16eb 100644 --- a/homeassistant/components/daikin/translations/it.json +++ b/homeassistant/components/daikin/translations/it.json @@ -5,6 +5,7 @@ "cannot_connect": "Impossibile connettersi" }, "error": { + "api_password": "Autenticazione non valida, utilizzare la chiave API o la password.", "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" diff --git a/homeassistant/components/daikin/translations/nl.json b/homeassistant/components/daikin/translations/nl.json index 706a81b5f7f..33659797e7a 100644 --- a/homeassistant/components/daikin/translations/nl.json +++ b/homeassistant/components/daikin/translations/nl.json @@ -5,6 +5,7 @@ "cannot_connect": "Kon niet verbinden" }, "error": { + "api_password": "Ongeldige authenticatie, gebruik API-sleutel of wachtwoord.", "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/daikin/translations/no.json b/homeassistant/components/daikin/translations/no.json index e63b8eeef0d..45914b51578 100644 --- a/homeassistant/components/daikin/translations/no.json +++ b/homeassistant/components/daikin/translations/no.json @@ -5,6 +5,7 @@ "cannot_connect": "Tilkobling mislyktes" }, "error": { + "api_password": "Ugyldig godkjenning, bruk enten API -n\u00f8kkel eller passord.", "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" diff --git a/homeassistant/components/daikin/translations/pl.json b/homeassistant/components/daikin/translations/pl.json index d11ffd4dd3a..c376bb0eb0e 100644 --- a/homeassistant/components/daikin/translations/pl.json +++ b/homeassistant/components/daikin/translations/pl.json @@ -5,6 +5,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { + "api_password": "Niepoprawne uwierzytelnienie, u\u017cyj klucza API albo has\u0142a.", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" diff --git a/homeassistant/components/daikin/translations/ru.json b/homeassistant/components/daikin/translations/ru.json index 7365bb0e7bb..45734a361fa 100644 --- a/homeassistant/components/daikin/translations/ru.json +++ b/homeassistant/components/daikin/translations/ru.json @@ -5,6 +5,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "error": { + "api_password": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043a\u043b\u044e\u0447 API \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", "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." diff --git a/homeassistant/components/daikin/translations/zh-Hant.json b/homeassistant/components/daikin/translations/zh-Hant.json index a6d4b4598b1..8072d135c20 100644 --- a/homeassistant/components/daikin/translations/zh-Hant.json +++ b/homeassistant/components/daikin/translations/zh-Hant.json @@ -5,6 +5,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { + "api_password": "\u9a57\u8b49\u78bc\u7121\u6548\u3001\u8acb\u4f7f\u7528 API \u5bc6\u9470\u6216\u5bc6\u78bc\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index e73d9b2e1be..228370be16a 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -1,6 +1,10 @@ """Support for Dark Sky weather service.""" +from __future__ import annotations + +from dataclasses import dataclass, field from datetime import timedelta import logging +from typing import Literal, NamedTuple import forecastio from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout @@ -10,6 +14,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA, SensorEntity, + SensorEntityDescription, ) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -20,9 +25,15 @@ from homeassistant.const import ( CONF_NAME, CONF_SCAN_INTERVAL, DEGREE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PRESSURE, LENGTH_CENTIMETERS, + LENGTH_INCHES, LENGTH_KILOMETERS, + LENGTH_MILES, PERCENTAGE, + PRECIPITATION_INCHES, PRECIPITATION_MILLIMETERS_PER_HOUR, PRESSURE_MBAR, SPEED_KILOMETERS_PER_HOUR, @@ -55,352 +66,423 @@ DEPRECATED_SENSOR_TYPES = { "temperature_min", } -# Sensor types are defined like so: -# Name, si unit, us unit, ca unit, uk unit, uk2 unit -SENSOR_TYPES = { - "summary": [ - "Summary", - None, - None, - None, - None, - None, - None, - ["currently", "hourly", "daily"], - ], - "minutely_summary": ["Minutely Summary", None, None, None, None, None, None, []], - "hourly_summary": ["Hourly Summary", None, None, None, None, None, None, []], - "daily_summary": ["Daily Summary", None, None, None, None, None, None, []], - "icon": [ - "Icon", - None, - None, - None, - None, - None, - None, - ["currently", "hourly", "daily"], - ], - "nearest_storm_distance": [ - "Nearest Storm Distance", - LENGTH_KILOMETERS, - "mi", - LENGTH_KILOMETERS, - LENGTH_KILOMETERS, - "mi", - "mdi:weather-lightning", - ["currently"], - ], - "nearest_storm_bearing": [ - "Nearest Storm Bearing", - DEGREE, - DEGREE, - DEGREE, - DEGREE, - DEGREE, - "mdi:weather-lightning", - ["currently"], - ], - "precip_type": [ - "Precip", - None, - None, - None, - None, - None, - "mdi:weather-pouring", - ["currently", "minutely", "hourly", "daily"], - ], - "precip_intensity": [ - "Precip Intensity", - PRECIPITATION_MILLIMETERS_PER_HOUR, - "in", - PRECIPITATION_MILLIMETERS_PER_HOUR, - PRECIPITATION_MILLIMETERS_PER_HOUR, - PRECIPITATION_MILLIMETERS_PER_HOUR, - "mdi:weather-rainy", - ["currently", "minutely", "hourly", "daily"], - ], - "precip_probability": [ - "Precip Probability", - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - "mdi:water-percent", - ["currently", "minutely", "hourly", "daily"], - ], - "precip_accumulation": [ - "Precip Accumulation", - LENGTH_CENTIMETERS, - "in", - LENGTH_CENTIMETERS, - LENGTH_CENTIMETERS, - LENGTH_CENTIMETERS, - "mdi:weather-snowy", - ["hourly", "daily"], - ], - "temperature": [ - "Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["currently", "hourly"], - ], - "apparent_temperature": [ - "Apparent Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["currently", "hourly"], - ], - "dew_point": [ - "Dew Point", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["currently", "hourly", "daily"], - ], - "wind_speed": [ - "Wind Speed", - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy", - ["currently", "hourly", "daily"], - ], - "wind_bearing": [ - "Wind Bearing", - DEGREE, - DEGREE, - DEGREE, - DEGREE, - DEGREE, - "mdi:compass", - ["currently", "hourly", "daily"], - ], - "wind_gust": [ - "Wind Gust", - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy-variant", - ["currently", "hourly", "daily"], - ], - "cloud_cover": [ - "Cloud Coverage", - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - "mdi:weather-partly-cloudy", - ["currently", "hourly", "daily"], - ], - "humidity": [ - "Humidity", - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - "mdi:water-percent", - ["currently", "hourly", "daily"], - ], - "pressure": [ - "Pressure", - PRESSURE_MBAR, - PRESSURE_MBAR, - PRESSURE_MBAR, - PRESSURE_MBAR, - PRESSURE_MBAR, - "mdi:gauge", - ["currently", "hourly", "daily"], - ], - "visibility": [ - "Visibility", - LENGTH_KILOMETERS, - "mi", - LENGTH_KILOMETERS, - LENGTH_KILOMETERS, - "mi", - "mdi:eye", - ["currently", "hourly", "daily"], - ], - "ozone": [ - "Ozone", - "DU", - "DU", - "DU", - "DU", - "DU", - "mdi:eye", - ["currently", "hourly", "daily"], - ], - "apparent_temperature_max": [ - "Daily High Apparent Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "apparent_temperature_high": [ - "Daytime High Apparent Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "apparent_temperature_min": [ - "Daily Low Apparent Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "apparent_temperature_low": [ - "Overnight Low Apparent Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "temperature_max": [ - "Daily High Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "temperature_high": [ - "Daytime High Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "temperature_min": [ - "Daily Low Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "temperature_low": [ - "Overnight Low Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "precip_intensity_max": [ - "Daily Max Precip Intensity", - PRECIPITATION_MILLIMETERS_PER_HOUR, - "in", - PRECIPITATION_MILLIMETERS_PER_HOUR, - PRECIPITATION_MILLIMETERS_PER_HOUR, - PRECIPITATION_MILLIMETERS_PER_HOUR, - "mdi:thermometer", - ["daily"], - ], - "uv_index": [ - "UV Index", - UV_INDEX, - UV_INDEX, - UV_INDEX, - UV_INDEX, - UV_INDEX, - "mdi:weather-sunny", - ["currently", "hourly", "daily"], - ], - "moon_phase": [ - "Moon Phase", - None, - None, - None, - None, - None, - "mdi:weather-night", - ["daily"], - ], - "sunrise_time": [ - "Sunrise", - None, - None, - None, - None, - None, - "mdi:white-balance-sunny", - ["daily"], - ], - "sunset_time": [ - "Sunset", - None, - None, - None, - None, - None, - "mdi:weather-night", - ["daily"], - ], - "alerts": ["Alerts", None, None, None, None, None, "mdi:alert-circle-outline", []], +MAP_UNIT_SYSTEM: dict[ + Literal["si", "us", "ca", "uk", "uk2"], + Literal["si_unit", "us_unit", "ca_unit", "uk_unit", "uk2_unit"], +] = { + "si": "si_unit", + "us": "us_unit", + "ca": "ca_unit", + "uk": "uk_unit", + "uk2": "uk2_unit", } -CONDITION_PICTURES = { - "clear-day": ["/static/images/darksky/weather-sunny.svg", "mdi:weather-sunny"], - "clear-night": ["/static/images/darksky/weather-night.svg", "mdi:weather-night"], - "rain": ["/static/images/darksky/weather-pouring.svg", "mdi:weather-pouring"], - "snow": ["/static/images/darksky/weather-snowy.svg", "mdi:weather-snowy"], - "sleet": ["/static/images/darksky/weather-hail.svg", "mdi:weather-snowy-rainy"], - "wind": ["/static/images/darksky/weather-windy.svg", "mdi:weather-windy"], - "fog": ["/static/images/darksky/weather-fog.svg", "mdi:weather-fog"], - "cloudy": ["/static/images/darksky/weather-cloudy.svg", "mdi:weather-cloudy"], - "partly-cloudy-day": [ - "/static/images/darksky/weather-partlycloudy.svg", - "mdi:weather-partly-cloudy", - ], - "partly-cloudy-night": [ - "/static/images/darksky/weather-cloudy.svg", - "mdi:weather-night-partly-cloudy", - ], + +@dataclass +class DarkskySensorEntityDescription(SensorEntityDescription): + """Describes Darksky sensor entity.""" + + si_unit: str | None = None + us_unit: str | None = None + ca_unit: str | None = None + uk_unit: str | None = None + uk2_unit: str | None = None + forecast_mode: list[str] = field(default_factory=list) + + +SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { + "summary": DarkskySensorEntityDescription( + key="summary", + name="Summary", + forecast_mode=["currently", "hourly", "daily"], + ), + "minutely_summary": DarkskySensorEntityDescription( + key="minutely_summary", + name="Minutely Summary", + forecast_mode=[], + ), + "hourly_summary": DarkskySensorEntityDescription( + key="hourly_summary", + name="Hourly Summary", + forecast_mode=[], + ), + "daily_summary": DarkskySensorEntityDescription( + key="daily_summary", + name="Daily Summary", + forecast_mode=[], + ), + "icon": DarkskySensorEntityDescription( + key="icon", + name="Icon", + forecast_mode=["currently", "hourly", "daily"], + ), + "nearest_storm_distance": DarkskySensorEntityDescription( + key="nearest_storm_distance", + name="Nearest Storm Distance", + si_unit=LENGTH_KILOMETERS, + us_unit=LENGTH_MILES, + ca_unit=LENGTH_KILOMETERS, + uk_unit=LENGTH_KILOMETERS, + uk2_unit=LENGTH_MILES, + icon="mdi:weather-lightning", + forecast_mode=["currently"], + ), + "nearest_storm_bearing": DarkskySensorEntityDescription( + key="nearest_storm_bearing", + name="Nearest Storm Bearing", + si_unit=DEGREE, + us_unit=DEGREE, + ca_unit=DEGREE, + uk_unit=DEGREE, + uk2_unit=DEGREE, + icon="mdi:weather-lightning", + forecast_mode=["currently"], + ), + "precip_type": DarkskySensorEntityDescription( + key="precip_type", + name="Precip", + icon="mdi:weather-pouring", + forecast_mode=["currently", "minutely", "hourly", "daily"], + ), + "precip_intensity": DarkskySensorEntityDescription( + key="precip_intensity", + name="Precip Intensity", + si_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + us_unit=PRECIPITATION_INCHES, + ca_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + uk_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + uk2_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + icon="mdi:weather-rainy", + forecast_mode=["currently", "minutely", "hourly", "daily"], + ), + "precip_probability": DarkskySensorEntityDescription( + key="precip_probability", + name="Precip Probability", + si_unit=PERCENTAGE, + us_unit=PERCENTAGE, + ca_unit=PERCENTAGE, + uk_unit=PERCENTAGE, + uk2_unit=PERCENTAGE, + icon="mdi:water-percent", + forecast_mode=["currently", "minutely", "hourly", "daily"], + ), + "precip_accumulation": DarkskySensorEntityDescription( + key="precip_accumulation", + name="Precip Accumulation", + si_unit=LENGTH_CENTIMETERS, + us_unit=LENGTH_INCHES, + ca_unit=LENGTH_CENTIMETERS, + uk_unit=LENGTH_CENTIMETERS, + uk2_unit=LENGTH_CENTIMETERS, + icon="mdi:weather-snowy", + forecast_mode=["hourly", "daily"], + ), + "temperature": DarkskySensorEntityDescription( + key="temperature", + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["currently", "hourly"], + ), + "apparent_temperature": DarkskySensorEntityDescription( + key="apparent_temperature", + name="Apparent Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["currently", "hourly"], + ), + "dew_point": DarkskySensorEntityDescription( + key="dew_point", + name="Dew Point", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["currently", "hourly", "daily"], + ), + "wind_speed": DarkskySensorEntityDescription( + key="wind_speed", + name="Wind Speed", + si_unit=SPEED_METERS_PER_SECOND, + us_unit=SPEED_MILES_PER_HOUR, + ca_unit=SPEED_KILOMETERS_PER_HOUR, + uk_unit=SPEED_MILES_PER_HOUR, + uk2_unit=SPEED_MILES_PER_HOUR, + icon="mdi:weather-windy", + forecast_mode=["currently", "hourly", "daily"], + ), + "wind_bearing": DarkskySensorEntityDescription( + key="wind_bearing", + name="Wind Bearing", + si_unit=DEGREE, + us_unit=DEGREE, + ca_unit=DEGREE, + uk_unit=DEGREE, + uk2_unit=DEGREE, + icon="mdi:compass", + forecast_mode=["currently", "hourly", "daily"], + ), + "wind_gust": DarkskySensorEntityDescription( + key="wind_gust", + name="Wind Gust", + si_unit=SPEED_METERS_PER_SECOND, + us_unit=SPEED_MILES_PER_HOUR, + ca_unit=SPEED_KILOMETERS_PER_HOUR, + uk_unit=SPEED_MILES_PER_HOUR, + uk2_unit=SPEED_MILES_PER_HOUR, + icon="mdi:weather-windy-variant", + forecast_mode=["currently", "hourly", "daily"], + ), + "cloud_cover": DarkskySensorEntityDescription( + key="cloud_cover", + name="Cloud Coverage", + si_unit=PERCENTAGE, + us_unit=PERCENTAGE, + ca_unit=PERCENTAGE, + uk_unit=PERCENTAGE, + uk2_unit=PERCENTAGE, + icon="mdi:weather-partly-cloudy", + forecast_mode=["currently", "hourly", "daily"], + ), + "humidity": DarkskySensorEntityDescription( + key="humidity", + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + si_unit=PERCENTAGE, + us_unit=PERCENTAGE, + ca_unit=PERCENTAGE, + uk_unit=PERCENTAGE, + uk2_unit=PERCENTAGE, + forecast_mode=["currently", "hourly", "daily"], + ), + "pressure": DarkskySensorEntityDescription( + key="pressure", + name="Pressure", + device_class=DEVICE_CLASS_PRESSURE, + si_unit=PRESSURE_MBAR, + us_unit=PRESSURE_MBAR, + ca_unit=PRESSURE_MBAR, + uk_unit=PRESSURE_MBAR, + uk2_unit=PRESSURE_MBAR, + forecast_mode=["currently", "hourly", "daily"], + ), + "visibility": DarkskySensorEntityDescription( + key="visibility", + name="Visibility", + si_unit=LENGTH_KILOMETERS, + us_unit=LENGTH_MILES, + ca_unit=LENGTH_KILOMETERS, + uk_unit=LENGTH_KILOMETERS, + uk2_unit=LENGTH_MILES, + icon="mdi:eye", + forecast_mode=["currently", "hourly", "daily"], + ), + "ozone": DarkskySensorEntityDescription( + key="ozone", + name="Ozone", + device_class=DEVICE_CLASS_OZONE, + si_unit="DU", + us_unit="DU", + ca_unit="DU", + uk_unit="DU", + uk2_unit="DU", + forecast_mode=["currently", "hourly", "daily"], + ), + "apparent_temperature_max": DarkskySensorEntityDescription( + key="apparent_temperature_max", + name="Daily High Apparent Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "apparent_temperature_high": DarkskySensorEntityDescription( + key="apparent_temperature_high", + name="Daytime High Apparent Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "apparent_temperature_min": DarkskySensorEntityDescription( + key="apparent_temperature_min", + name="Daily Low Apparent Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "apparent_temperature_low": DarkskySensorEntityDescription( + key="apparent_temperature_low", + name="Overnight Low Apparent Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "temperature_max": DarkskySensorEntityDescription( + key="temperature_max", + name="Daily High Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "temperature_high": DarkskySensorEntityDescription( + key="temperature_high", + name="Daytime High Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "temperature_min": DarkskySensorEntityDescription( + key="temperature_min", + name="Daily Low Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "temperature_low": DarkskySensorEntityDescription( + key="temperature_low", + name="Overnight Low Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "precip_intensity_max": DarkskySensorEntityDescription( + key="precip_intensity_max", + name="Daily Max Precip Intensity", + si_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + us_unit=PRECIPITATION_INCHES, + ca_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + uk_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + uk2_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + icon="mdi:thermometer", + forecast_mode=["daily"], + ), + "uv_index": DarkskySensorEntityDescription( + key="uv_index", + name="UV Index", + si_unit=UV_INDEX, + us_unit=UV_INDEX, + ca_unit=UV_INDEX, + uk_unit=UV_INDEX, + uk2_unit=UV_INDEX, + icon="mdi:weather-sunny", + forecast_mode=["currently", "hourly", "daily"], + ), + "moon_phase": DarkskySensorEntityDescription( + key="moon_phase", + name="Moon Phase", + icon="mdi:weather-night", + forecast_mode=["daily"], + ), + "sunrise_time": DarkskySensorEntityDescription( + key="sunrise_time", + name="Sunrise", + icon="mdi:white-balance-sunny", + forecast_mode=["daily"], + ), + "sunset_time": DarkskySensorEntityDescription( + key="sunset_time", + name="Sunset", + icon="mdi:weather-night", + forecast_mode=["daily"], + ), + "alerts": DarkskySensorEntityDescription( + key="alerts", + name="Alerts", + icon="mdi:alert-circle-outline", + forecast_mode=[], + ), +} + + +class ConditionPicture(NamedTuple): + """Entity picture and icon for condition.""" + + entity_picture: str + icon: str + + +CONDITION_PICTURES: dict[str, ConditionPicture] = { + "clear-day": ConditionPicture( + entity_picture="/static/images/darksky/weather-sunny.svg", + icon="mdi:weather-sunny", + ), + "clear-night": ConditionPicture( + entity_picture="/static/images/darksky/weather-night.svg", + icon="mdi:weather-night", + ), + "rain": ConditionPicture( + entity_picture="/static/images/darksky/weather-pouring.svg", + icon="mdi:weather-pouring", + ), + "snow": ConditionPicture( + entity_picture="/static/images/darksky/weather-snowy.svg", + icon="mdi:weather-snowy", + ), + "sleet": ConditionPicture( + entity_picture="/static/images/darksky/weather-hail.svg", + icon="mdi:weather-snowy-rainy", + ), + "wind": ConditionPicture( + entity_picture="/static/images/darksky/weather-windy.svg", + icon="mdi:weather-windy", + ), + "fog": ConditionPicture( + entity_picture="/static/images/darksky/weather-fog.svg", + icon="mdi:weather-fog", + ), + "cloudy": ConditionPicture( + entity_picture="/static/images/darksky/weather-cloudy.svg", + icon="mdi:weather-cloudy", + ), + "partly-cloudy-day": ConditionPicture( + entity_picture="/static/images/darksky/weather-partlycloudy.svg", + icon="mdi:weather-partly-cloudy", + ), + "partly-cloudy-night": ConditionPicture( + entity_picture="/static/images/darksky/weather-cloudy.svg", + icon="mdi:weather-night-partly-cloudy", + ), } # Language Supported Codes @@ -523,26 +605,31 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for variable in config[CONF_MONITORED_CONDITIONS]: if variable in DEPRECATED_SENSOR_TYPES: _LOGGER.warning("Monitored condition %s is deprecated", variable) - if not SENSOR_TYPES[variable][7] or "currently" in SENSOR_TYPES[variable][7]: + description = SENSOR_TYPES[variable] + if not description.forecast_mode or "currently" in description.forecast_mode: if variable == "alerts": - sensors.append(DarkSkyAlertSensor(forecast_data, variable, name)) + sensors.append(DarkSkyAlertSensor(forecast_data, description, name)) else: - sensors.append(DarkSkySensor(forecast_data, variable, name)) + sensors.append(DarkSkySensor(forecast_data, description, name)) - if forecast is not None and "daily" in SENSOR_TYPES[variable][7]: - for forecast_day in forecast: - sensors.append( + if forecast is not None and "daily" in description.forecast_mode: + sensors.extend( + [ DarkSkySensor( - forecast_data, variable, name, forecast_day=forecast_day + forecast_data, description, name, forecast_day=forecast_day ) - ) - if forecast_hour is not None and "hourly" in SENSOR_TYPES[variable][7]: - for forecast_h in forecast_hour: - sensors.append( + for forecast_day in forecast + ] + ) + if forecast_hour is not None and "hourly" in description.forecast_mode: + sensors.extend( + [ DarkSkySensor( - forecast_data, variable, name, forecast_hour=forecast_h + forecast_data, description, name, forecast_hour=forecast_h ) - ) + for forecast_h in forecast_hour + ] + ) add_entities(sensors, True) @@ -550,33 +637,31 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DarkSkySensor(SensorEntity): """Implementation of a Dark Sky sensor.""" + entity_description: DarkskySensorEntityDescription + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + def __init__( - self, forecast_data, sensor_type, name, forecast_day=None, forecast_hour=None + self, + forecast_data, + description: DarkskySensorEntityDescription, + name, + forecast_day=None, + forecast_hour=None, ): """Initialize the sensor.""" - self.client_name = name - self._name = SENSOR_TYPES[sensor_type][0] + self.entity_description = description self.forecast_data = forecast_data - self.type = sensor_type self.forecast_day = forecast_day self.forecast_hour = forecast_hour - self._state = None self._icon = None self._unit_of_measurement = None - @property - def name(self): - """Return the name of the sensor.""" - if self.forecast_day is not None: - return f"{self.client_name} {self._name} {self.forecast_day}d" - if self.forecast_hour is not None: - return f"{self.client_name} {self._name} {self.forecast_hour}h" - return f"{self.client_name} {self._name}" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state + if forecast_day is not None: + self._attr_name = f"{name} {description.name} {forecast_day}d" + elif forecast_hour is not None: + self._attr_name = f"{name} {description.name} {forecast_hour}h" + else: + self._attr_name = f"{name} {description.name}" @property def native_unit_of_measurement(self): @@ -591,41 +676,29 @@ class DarkSkySensor(SensorEntity): @property def entity_picture(self): """Return the entity picture to use in the frontend, if any.""" - if self._icon is None or "summary" not in self.type: + if self._icon is None or "summary" not in self.entity_description.key: return None if self._icon in CONDITION_PICTURES: - return CONDITION_PICTURES[self._icon][0] + return CONDITION_PICTURES[self._icon].entity_picture return None def update_unit_of_measurement(self): """Update units based on unit system.""" - unit_index = {"si": 1, "us": 2, "ca": 3, "uk": 4, "uk2": 5}.get( - self.unit_system, 1 - ) - self._unit_of_measurement = SENSOR_TYPES[self.type][unit_index] + unit_key = MAP_UNIT_SYSTEM.get(self.unit_system, "si_unit") + self._unit_of_measurement = getattr(self.entity_description, unit_key) @property def icon(self): """Icon to use in the frontend, if any.""" - if "summary" in self.type and self._icon in CONDITION_PICTURES: - return CONDITION_PICTURES[self._icon][1] + if ( + "summary" in self.entity_description.key + and self._icon in CONDITION_PICTURES + ): + return CONDITION_PICTURES[self._icon].icon - return SENSOR_TYPES[self.type][6] - - @property - def device_class(self): - """Device class of the entity.""" - if SENSOR_TYPES[self.type][1] == TEMP_CELSIUS: - return DEVICE_CLASS_TEMPERATURE - - return None - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} + return self.entity_description.icon def update(self): """Get the latest data from Dark Sky and updates the states.""" @@ -636,39 +709,42 @@ class DarkSkySensor(SensorEntity): self.forecast_data.update() self.update_unit_of_measurement() - if self.type == "minutely_summary": + sensor_type = self.entity_description.key + if sensor_type == "minutely_summary": self.forecast_data.update_minutely() minutely = self.forecast_data.data_minutely - self._state = getattr(minutely, "summary", "") + self._attr_native_value = getattr(minutely, "summary", "") self._icon = getattr(minutely, "icon", "") - elif self.type == "hourly_summary": + elif sensor_type == "hourly_summary": self.forecast_data.update_hourly() hourly = self.forecast_data.data_hourly - self._state = getattr(hourly, "summary", "") + self._attr_native_value = getattr(hourly, "summary", "") self._icon = getattr(hourly, "icon", "") elif self.forecast_hour is not None: self.forecast_data.update_hourly() hourly = self.forecast_data.data_hourly if hasattr(hourly, "data"): - self._state = self.get_state(hourly.data[self.forecast_hour]) + self._attr_native_value = self.get_state( + hourly.data[self.forecast_hour] + ) else: - self._state = 0 - elif self.type == "daily_summary": + self._attr_native_value = 0 + elif sensor_type == "daily_summary": self.forecast_data.update_daily() daily = self.forecast_data.data_daily - self._state = getattr(daily, "summary", "") + self._attr_native_value = getattr(daily, "summary", "") self._icon = getattr(daily, "icon", "") elif self.forecast_day is not None: self.forecast_data.update_daily() daily = self.forecast_data.data_daily if hasattr(daily, "data"): - self._state = self.get_state(daily.data[self.forecast_day]) + self._attr_native_value = self.get_state(daily.data[self.forecast_day]) else: - self._state = 0 + self._attr_native_value = 0 else: self.forecast_data.update_currently() currently = self.forecast_data.data_currently - self._state = self.get_state(currently) + self._attr_native_value = self.get_state(currently) def get_state(self, data): """ @@ -676,21 +752,22 @@ class DarkSkySensor(SensorEntity): If the sensor type is unknown, the current state is returned. """ - lookup_type = convert_to_camel(self.type) + sensor_type = self.entity_description.key + lookup_type = convert_to_camel(sensor_type) state = getattr(data, lookup_type, None) if state is None: return state - if "summary" in self.type: + if "summary" in sensor_type: self._icon = getattr(data, "icon", "") # Some state data needs to be rounded to whole values or converted to # percentages - if self.type in ["precip_probability", "cloud_cover", "humidity"]: + if sensor_type in {"precip_probability", "cloud_cover", "humidity"}: return round(state * 100, 1) - if self.type in [ + if sensor_type in { "dew_point", "temperature", "apparent_temperature", @@ -706,7 +783,7 @@ class DarkSkySensor(SensorEntity): "pressure", "ozone", "uvIndex", - ]: + }: return round(state, 1) return state @@ -714,30 +791,23 @@ class DarkSkySensor(SensorEntity): class DarkSkyAlertSensor(SensorEntity): """Implementation of a Dark Sky sensor.""" - def __init__(self, forecast_data, sensor_type, name): + entity_description: DarkskySensorEntityDescription + _attr_native_value: int | None + + def __init__( + self, forecast_data, description: DarkskySensorEntityDescription, name + ): """Initialize the sensor.""" - self.client_name = name - self._name = SENSOR_TYPES[sensor_type][0] + self.entity_description = description self.forecast_data = forecast_data - self.type = sensor_type - self._state = None - self._icon = None self._alerts = None - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state + self._attr_name = f"{name} {description.name}" @property def icon(self): """Icon to use in the frontend, if any.""" - if self._state is not None and self._state > 0: + if self._attr_native_value is not None and self._attr_native_value > 0: return "mdi:alert-circle" return "mdi:alert-circle-outline" @@ -755,7 +825,7 @@ class DarkSkyAlertSensor(SensorEntity): self.forecast_data.update() self.forecast_data.update_alerts() alerts = self.forecast_data.data_alerts - self._state = self.get_state(alerts) + self._attr_native_value = self.get_state(alerts) def get_state(self, data): """ diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index 0ad448ddfbd..b5aafd24c3d 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -91,8 +91,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config.get(CONF_NAME) mode = config.get(CONF_MODE) - units = config.get(CONF_UNITS) - if not units: + if not (units := config.get(CONF_UNITS)): units = "ca" if hass.config.units.is_metric else "us" dark_sky = DarkSkyData(config.get(CONF_API_KEY), latitude, longitude, units) diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index bc52e7712b6..303cfe72b97 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -1,4 +1,5 @@ """Support for DD-WRT routers.""" +from http import HTTPStatus import logging import re @@ -16,8 +17,6 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - HTTP_OK, - HTTP_UNAUTHORIZED, ) import homeassistant.helpers.config_validation as cv @@ -88,9 +87,7 @@ class DdWrtDeviceScanner(DeviceScanner): if not data: return None - dhcp_leases = data.get("dhcp_leases") - - if not dhcp_leases: + if not (dhcp_leases := data.get("dhcp_leases")): return None # Remove leading and trailing quotes and spaces @@ -154,9 +151,9 @@ class DdWrtDeviceScanner(DeviceScanner): except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") return - if response.status_code == HTTP_OK: + if response.status_code == HTTPStatus.OK: return _parse_ddwrt_response(response.text) - if response.status_code == HTTP_UNAUTHORIZED: + if response.status_code == HTTPStatus.UNAUTHORIZED: # Authentication error _LOGGER.exception( "Failed to authenticate, check your username and password" diff --git a/homeassistant/components/debugpy/__init__.py b/homeassistant/components/debugpy/__init__.py index 613ecfd8ffa..21cfeb15a80 100644 --- a/homeassistant/components/debugpy/__init__.py +++ b/homeassistant/components/debugpy/__init__.py @@ -48,8 +48,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: debugpy.listen((conf[CONF_HOST], conf[CONF_PORT])) - wait = conf[CONF_WAIT] - if wait: + if conf[CONF_WAIT]: _LOGGER.warning( "Waiting for remote debug connection on %s:%s", conf[CONF_HOST], diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 8d6cab13e62..c8da95a006f 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.4.3"], + "requirements": ["debugpy==1.5.0"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 1b9a418fb29..47a70a43ae2 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -32,12 +32,13 @@ async def async_setup_entry(hass, config_entry): if not await gateway.async_setup(): return False + if not hass.data[DOMAIN]: + async_setup_services(hass) + hass.data[DOMAIN][config_entry.entry_id] = gateway await gateway.async_update_device_registry() - await async_setup_services(hass) - config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) ) @@ -50,7 +51,7 @@ async def async_unload_entry(hass, config_entry): gateway = hass.data[DOMAIN].pop(config_entry.entry_id) if not hass.data[DOMAIN]: - await async_unload_services(hass) + async_unload_services(hass) elif gateway.master: await async_update_master_gateway(hass, config_entry) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index 73e85f13713..823c9c67654 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -35,7 +35,6 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -89,7 +88,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: config_entry.async_on_unload( async_dispatcher_connect( hass, - gateway.async_signal_new_device(NEW_SENSOR), + gateway.signal_new_sensor, async_add_alarm_control_panel, ) ) @@ -113,14 +112,14 @@ class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): self.alarm_system = get_alarm_system_for_unique_id(gateway, device.unique_id) @callback - def async_update_callback(self, force_update: bool = False) -> None: + def async_update_callback(self) -> None: """Update the control panels state.""" keys = {"panel", "reachable"} - if force_update or ( + if ( self._device.changed_keys.intersection(keys) and self._device.state in DECONZ_TO_ALARM_STATE ): - super().async_update_callback(force_update=force_update) + super().async_update_callback() @property def state(self) -> str | None: diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 33b68f25cab..7f77bbb8809 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -15,18 +15,18 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, - DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SMOKE, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_VIBRATION, DOMAIN, BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR +from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -105,7 +105,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor + hass, + gateway.signal_new_sensor, + async_add_sensor, ) ) @@ -127,11 +129,11 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): self.entity_description = entity_description @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self): """Update the sensor's state.""" keys = {"on", "reachable", "state"} - if force_update or self._device.changed_keys.intersection(keys): - super().async_update_callback(force_update=force_update) + if self._device.changed_keys.intersection(keys): + super().async_update_callback() @property def is_on(self): @@ -149,12 +151,12 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): if self._device.secondary_temperature is not None: attr[ATTR_TEMPERATURE] = self._device.secondary_temperature - if self._device.type in Presence.ZHATYPE: + if isinstance(self._device, Presence): if self._device.dark is not None: attr[ATTR_DARK] = self._device.dark - elif self._device.type in Vibration.ZHATYPE: + elif isinstance(self._device, Vibration): attr[ATTR_ORIENTATION] = self._device.orientation attr[ATTR_TILTANGLE] = self._device.tilt_angle attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibration_strength @@ -167,7 +169,8 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity): TYPE = DOMAIN - _attr_device_class = DEVICE_CLASS_PROBLEM + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_device_class = DEVICE_CLASS_TAMPER def __init__(self, device, gateway): """Initialize deCONZ binary sensor.""" @@ -181,11 +184,11 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity): return f"{self.serial}-tampered" @callback - def async_update_callback(self, force_update: bool = False) -> None: + def async_update_callback(self) -> None: """Update the sensor's state.""" keys = {"tampered", "reachable"} - if force_update or self._device.changed_keys.intersection(keys): - super().async_update_callback(force_update=force_update) + if self._device.changed_keys.intersection(keys): + super().async_update_callback() @property def is_on(self) -> bool: diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 86c19be3cd2..b9401e6d5a3 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -1,7 +1,27 @@ """Support for deCONZ climate devices.""" from __future__ import annotations -from pydeconz.sensor import Thermostat +from pydeconz.sensor import ( + THERMOSTAT_FAN_MODE_AUTO, + THERMOSTAT_FAN_MODE_HIGH, + THERMOSTAT_FAN_MODE_LOW, + THERMOSTAT_FAN_MODE_MEDIUM, + THERMOSTAT_FAN_MODE_OFF, + THERMOSTAT_FAN_MODE_ON, + THERMOSTAT_FAN_MODE_SMART, + THERMOSTAT_MODE_AUTO, + THERMOSTAT_MODE_COOL, + THERMOSTAT_MODE_HEAT, + THERMOSTAT_MODE_OFF, + THERMOSTAT_PRESET_AUTO, + THERMOSTAT_PRESET_BOOST, + THERMOSTAT_PRESET_COMFORT, + THERMOSTAT_PRESET_COMPLEX, + THERMOSTAT_PRESET_ECO, + THERMOSTAT_PRESET_HOLIDAY, + THERMOSTAT_PRESET_MANUAL, + Thermostat, +) from homeassistant.components.climate import DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( @@ -26,29 +46,29 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR +from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry DECONZ_FAN_SMART = "smart" FAN_MODE_TO_DECONZ = { - DECONZ_FAN_SMART: "smart", - FAN_AUTO: "auto", - FAN_HIGH: "high", - FAN_MEDIUM: "medium", - FAN_LOW: "low", - FAN_ON: "on", - FAN_OFF: "off", + DECONZ_FAN_SMART: THERMOSTAT_FAN_MODE_SMART, + FAN_AUTO: THERMOSTAT_FAN_MODE_AUTO, + FAN_HIGH: THERMOSTAT_FAN_MODE_HIGH, + FAN_MEDIUM: THERMOSTAT_FAN_MODE_MEDIUM, + FAN_LOW: THERMOSTAT_FAN_MODE_LOW, + FAN_ON: THERMOSTAT_FAN_MODE_ON, + FAN_OFF: THERMOSTAT_FAN_MODE_OFF, } DECONZ_TO_FAN_MODE = {value: key for key, value in FAN_MODE_TO_DECONZ.items()} HVAC_MODE_TO_DECONZ = { - HVAC_MODE_AUTO: "auto", - HVAC_MODE_COOL: "cool", - HVAC_MODE_HEAT: "heat", - HVAC_MODE_OFF: "off", + HVAC_MODE_AUTO: THERMOSTAT_MODE_AUTO, + HVAC_MODE_COOL: THERMOSTAT_MODE_COOL, + HVAC_MODE_HEAT: THERMOSTAT_MODE_HEAT, + HVAC_MODE_OFF: THERMOSTAT_MODE_OFF, } DECONZ_PRESET_AUTO = "auto" @@ -57,13 +77,13 @@ DECONZ_PRESET_HOLIDAY = "holiday" DECONZ_PRESET_MANUAL = "manual" PRESET_MODE_TO_DECONZ = { - DECONZ_PRESET_AUTO: "auto", - PRESET_BOOST: "boost", - PRESET_COMFORT: "comfort", - DECONZ_PRESET_COMPLEX: "complex", - PRESET_ECO: "eco", - DECONZ_PRESET_HOLIDAY: "holiday", - DECONZ_PRESET_MANUAL: "manual", + DECONZ_PRESET_AUTO: THERMOSTAT_PRESET_AUTO, + PRESET_BOOST: THERMOSTAT_PRESET_BOOST, + PRESET_COMFORT: THERMOSTAT_PRESET_COMFORT, + DECONZ_PRESET_COMPLEX: THERMOSTAT_PRESET_COMPLEX, + PRESET_ECO: THERMOSTAT_PRESET_ECO, + DECONZ_PRESET_HOLIDAY: THERMOSTAT_PRESET_HOLIDAY, + DECONZ_PRESET_MANUAL: THERMOSTAT_PRESET_MANUAL, } DECONZ_TO_PRESET_MODE = {value: key for key, value in PRESET_MODE_TO_DECONZ.items()} @@ -98,7 +118,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_climate + hass, + gateway.signal_new_sensor, + async_add_climate, ) ) @@ -116,7 +138,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): super().__init__(device, gateway) self._hvac_mode_to_deconz = dict(HVAC_MODE_TO_DECONZ) - if "mode" not in device.raw["config"]: + if not device.mode: self._hvac_mode_to_deconz = { HVAC_MODE_HEAT: True, HVAC_MODE_OFF: False, @@ -129,10 +151,10 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE - if "fanmode" in device.raw["config"]: + if device.fan_mode: self._attr_supported_features |= SUPPORT_FAN_MODE - if "preset" in device.raw["config"]: + if device.preset: self._attr_supported_features |= SUPPORT_PRESET_MODE # Fan control @@ -214,7 +236,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): @property def target_temperature(self) -> float: """Return the target temperature.""" - if self._device.mode == "cool": + if self._device.mode == THERMOSTAT_MODE_COOL: return self._device.cooling_setpoint return self._device.heating_setpoint diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 67753aa0355..09f0cd15141 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -10,6 +10,7 @@ from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN @@ -40,17 +41,13 @@ PLATFORMS = [ FAN_DOMAIN, LIGHT_DOMAIN, LOCK_DOMAIN, + NUMBER_DOMAIN, SCENE_DOMAIN, SENSOR_DOMAIN, SIREN_DOMAIN, SWITCH_DOMAIN, ] -NEW_GROUP = "groups" -NEW_LIGHT = "lights" -NEW_SCENE = "scenes" -NEW_SENSOR = "sensors" - ATTR_DARK = "dark" ATTR_LOCKED = "locked" ATTR_OFFSET = "offset" diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index abf1fe4eea4..5cf90c4dca1 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -21,7 +21,6 @@ from homeassistant.components.cover import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import NEW_LIGHT from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -54,7 +53,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_cover + hass, + gateway.signal_new_light, + async_add_cover, ) ) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index fe9eaa8ff60..bbd4051c177 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -1,9 +1,10 @@ """Base class for deCONZ devices.""" +from __future__ import annotations from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as DECONZ_DOMAIN @@ -30,20 +31,20 @@ class DeconzBase: return self._device.unique_id.split("-", 1)[0] @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return a device description for device registry.""" if self.serial is None: return None - return { - "connections": {(CONNECTION_ZIGBEE, self.serial)}, - "identifiers": {(DECONZ_DOMAIN, self.serial)}, - "manufacturer": self._device.manufacturer, - "model": self._device.model_id, - "name": self._device.name, - "sw_version": self._device.software_version, - "via_device": (DECONZ_DOMAIN, self.gateway.api.config.bridge_id), - } + return DeviceInfo( + connections={(CONNECTION_ZIGBEE, self.serial)}, + identifiers={(DECONZ_DOMAIN, self.serial)}, + manufacturer=self._device.manufacturer, + model=self._device.model_id, + name=self._device.name, + sw_version=self._device.software_version, + via_device=(DECONZ_DOMAIN, self.gateway.api.config.bridge_id), + ) class DeconzDevice(DeconzBase, Entity): @@ -66,7 +67,9 @@ class DeconzDevice(DeconzBase, Entity): self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id self.async_on_remove( async_dispatcher_connect( - self.hass, self.gateway.signal_reachable, self.async_update_callback + self.hass, + self.gateway.signal_reachable, + self.async_update_connection_state, ) ) @@ -77,9 +80,14 @@ class DeconzDevice(DeconzBase, Entity): self.gateway.entities[self.TYPE].remove(self.unique_id) @callback - def async_update_callback(self, force_update=False): + def async_update_connection_state(self): + """Update the device's available state.""" + self.async_write_ha_state() + + @callback + def async_update_callback(self): """Update the device's state.""" - if not force_update and self.gateway.ignore_state_updates: + if self.gateway.ignore_state_updates: return self.async_write_ha_state() diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 2d9799c4d02..300aef3f82a 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -17,10 +17,11 @@ from homeassistant.const import ( CONF_XY, ) from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify -from .const import CONF_ANGLE, CONF_GESTURE, LOGGER, NEW_SENSOR +from .const import CONF_ANGLE, CONF_GESTURE, LOGGER from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" @@ -63,7 +64,9 @@ async def async_setup_events(gateway) -> None: gateway.config_entry.async_on_unload( async_dispatcher_connect( - gateway.hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor + gateway.hass, + gateway.signal_new_sensor, + async_add_sensor, ) ) @@ -107,7 +110,7 @@ class DeconzEvent(DeconzBase): self._device.remove_callback(self.async_update_callback) @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self): """Fire the event if reason is that state is updated.""" if ( self.gateway.ignore_state_updates @@ -140,9 +143,7 @@ class DeconzEvent(DeconzBase): if not self.device_info: return - device_registry = ( - await self.gateway.hass.helpers.device_registry.async_get_registry() - ) + device_registry = dr.async_get(self.gateway.hass) entry = device_registry.async_get_or_create( config_entry_id=self.gateway.config_entry.entry_id, **self.device_info @@ -154,7 +155,7 @@ class DeconzAlarmEvent(DeconzEvent): """Alarm control panel companion event when user interacts with a keypad.""" @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self): """Fire the event if reason is new action is updated.""" if ( self.gateway.ignore_state_updates diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 8234ed81aed..d1abbed0928 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_TYPE, CONF_UNIQUE_ID, ) +from homeassistant.helpers import device_registry as dr from . import DOMAIN from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE @@ -628,7 +629,7 @@ async def async_validate_trigger_config(hass, config): """Validate config.""" config = TRIGGER_SCHEMA(config) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get(config[CONF_DEVICE_ID]) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) @@ -650,7 +651,7 @@ async def async_validate_trigger_config(hass, config): async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get(config[CONF_DEVICE_ID]) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) @@ -684,7 +685,7 @@ async def async_get_triggers(hass, device_id): Retrieve the deconz event object matching device entry. Generate device trigger list. """ - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get(device_id) if device.model not in REMOTES: diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 38fc087cdfd..40862bfcde1 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -1,7 +1,14 @@ """Support for deCONZ fans.""" from __future__ import annotations -from pydeconz.light import Fan +from pydeconz.light import ( + FAN_SPEED_25_PERCENT, + FAN_SPEED_50_PERCENT, + FAN_SPEED_75_PERCENT, + FAN_SPEED_100_PERCENT, + FAN_SPEED_OFF, + Fan, +) from homeassistant.components.fan import ( DOMAIN, @@ -19,14 +26,28 @@ from homeassistant.util.percentage import ( percentage_to_ordered_list_item, ) -from .const import NEW_LIGHT from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -ORDERED_NAMED_FAN_SPEEDS = [1, 2, 3, 4] +ORDERED_NAMED_FAN_SPEEDS = [ + FAN_SPEED_25_PERCENT, + FAN_SPEED_50_PERCENT, + FAN_SPEED_75_PERCENT, + FAN_SPEED_100_PERCENT, +] -LEGACY_SPEED_TO_DECONZ = {SPEED_OFF: 0, SPEED_LOW: 1, SPEED_MEDIUM: 2, SPEED_HIGH: 4} -LEGACY_DECONZ_TO_SPEED = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 4: SPEED_HIGH} +LEGACY_SPEED_TO_DECONZ = { + SPEED_OFF: FAN_SPEED_OFF, + SPEED_LOW: FAN_SPEED_25_PERCENT, + SPEED_MEDIUM: FAN_SPEED_50_PERCENT, + SPEED_HIGH: FAN_SPEED_100_PERCENT, +} +LEGACY_DECONZ_TO_SPEED = { + FAN_SPEED_OFF: SPEED_OFF, + FAN_SPEED_25_PERCENT: SPEED_LOW, + FAN_SPEED_50_PERCENT: SPEED_MEDIUM, + FAN_SPEED_100_PERCENT: SPEED_HIGH, +} async def async_setup_entry(hass, config_entry, async_add_entities) -> None: @@ -52,7 +73,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_fan + hass, + gateway.signal_new_light, + async_add_fan, ) ) @@ -68,7 +91,7 @@ class DeconzFan(DeconzDevice, FanEntity): """Set up fan.""" super().__init__(device, gateway) - self._default_on_speed = 2 + self._default_on_speed = FAN_SPEED_50_PERCENT if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: self._default_on_speed = self._device.speed @@ -77,12 +100,12 @@ class DeconzFan(DeconzDevice, FanEntity): @property def is_on(self) -> bool: """Return true if fan is on.""" - return self._device.speed != 0 + return self._device.speed != FAN_SPEED_OFF @property def percentage(self) -> int | None: """Return the current speed percentage.""" - if self._device.speed == 0: + if self._device.speed == FAN_SPEED_OFF: return 0 if self._device.speed not in ORDERED_NAMED_FAN_SPEEDS: return None @@ -136,11 +159,11 @@ class DeconzFan(DeconzDevice, FanEntity): return self._attr_supported_features @callback - def async_update_callback(self, force_update=False) -> None: + def async_update_callback(self) -> None: """Store latest configured speed from the device.""" if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: self._default_on_speed = self._device.speed - super().async_update_callback(force_update) + super().async_update_callback() async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" @@ -177,4 +200,4 @@ class DeconzFan(DeconzDevice, FanEntity): async def async_turn_off(self, **kwargs) -> None: """Turn off fan.""" - await self._device.set_speed(0) + await self._device.set_speed(FAN_SPEED_OFF) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index ecbc36ebadc..ddb0d47190c 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -2,12 +2,17 @@ import asyncio import async_timeout -from pydeconz import DeconzSession, errors +from pydeconz import DeconzSession, errors, group, light, sensor +from homeassistant.config_entries import SOURCE_HASSIO from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import ( + aiohttp_client, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -21,10 +26,6 @@ from .const import ( DEFAULT_ALLOW_NEW_DEVICES, DOMAIN as DECONZ_DOMAIN, LOGGER, - NEW_GROUP, - NEW_LIGHT, - NEW_SCENE, - NEW_SENSOR, PLATFORMS, ) from .deconz_event import async_setup_events, async_unload_events @@ -50,6 +51,20 @@ class DeconzGateway: self.available = True self.ignore_state_updates = False + self.signal_reachable = f"deconz-reachable-{config_entry.entry_id}" + + self.signal_new_group = f"deconz_new_group_{config_entry.entry_id}" + self.signal_new_light = f"deconz_new_light_{config_entry.entry_id}" + self.signal_new_scene = f"deconz_new_scene_{config_entry.entry_id}" + self.signal_new_sensor = f"deconz_new_sensor_{config_entry.entry_id}" + + self.deconz_resource_type_to_signal_new_device = { + group.RESOURCE_TYPE: self.signal_new_group, + light.RESOURCE_TYPE: self.signal_new_light, + group.RESOURCE_TYPE_SCENE: self.signal_new_scene, + sensor.RESOURCE_TYPE: self.signal_new_sensor, + } + self.deconz_ids = {} self.entities = {} self.events = [] @@ -92,24 +107,6 @@ class DeconzGateway: CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES ) - # Signals - - @property - def signal_reachable(self) -> str: - """Gateway specific event to signal a change in connection status.""" - return f"deconz-reachable-{self.bridgeid}" - - @callback - def async_signal_new_device(self, device_type) -> str: - """Gateway specific event to signal new device.""" - new_device = { - NEW_GROUP: f"deconz_new_group_{self.bridgeid}", - NEW_LIGHT: f"deconz_new_light_{self.bridgeid}", - NEW_SCENE: f"deconz_new_scene_{self.bridgeid}", - NEW_SENSOR: f"deconz_new_sensor_{self.bridgeid}", - } - return new_device[device_type] - # Callbacks @callback @@ -117,14 +114,18 @@ class DeconzGateway: """Handle signals of gateway connection status.""" self.available = available self.ignore_state_updates = False - async_dispatcher_send(self.hass, self.signal_reachable, True) + async_dispatcher_send(self.hass, self.signal_reachable) @callback def async_add_device_callback( - self, device_type, device=None, force: bool = False + self, resource_type, device=None, force: bool = False ) -> None: """Handle event of new device creation in deCONZ.""" - if not force and not self.option_allow_new_devices: + if ( + not force + and not self.option_allow_new_devices + or resource_type not in self.deconz_resource_type_to_signal_new_device + ): return args = [] @@ -134,13 +135,13 @@ class DeconzGateway: async_dispatcher_send( self.hass, - self.async_signal_new_device(device_type), + self.deconz_resource_type_to_signal_new_device[resource_type], *args, # Don't send device if None, it would override default value in listeners ) async def async_update_device_registry(self) -> None: """Update device registry.""" - device_registry = await self.hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(self.hass) # Host device device_registry.async_get_or_create( @@ -149,8 +150,13 @@ class DeconzGateway: ) # Gateway service + configuration_url = f"http://{self.host}:{self.config_entry.data[CONF_PORT]}" + if self.config_entry.source == SOURCE_HASSIO: + configuration_url = None device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, + configuration_url=configuration_url, + entry_type="service", identifiers={(DECONZ_DOMAIN, self.api.config.bridge_id)}, manufacturer="Dresden Elektronik", model=self.api.config.model_id, @@ -207,7 +213,7 @@ class DeconzGateway: deconz_ids = [] if self.option_allow_clip_sensor: - self.async_add_device_callback(NEW_SENSOR) + self.async_add_device_callback(sensor.RESOURCE_TYPE) else: deconz_ids += [ @@ -217,12 +223,12 @@ class DeconzGateway: ] if self.option_allow_deconz_groups: - self.async_add_device_callback(NEW_GROUP) + self.async_add_device_callback(group.RESOURCE_TYPE) else: deconz_ids += [group.deconz_id for group in self.api.groups.values()] - entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(self.hass) for entity_id, deconz_id in self.deconz_ids.items(): if deconz_id in deconz_ids and entity_registry.async_is_registered( diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 2202bdbe58f..6bb4f5c5b00 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -2,7 +2,14 @@ from __future__ import annotations -from pydeconz.light import Light +from pydeconz.group import DeconzGroup as Group +from pydeconz.light import ( + ALERT_LONG, + ALERT_SHORT, + EFFECT_COLOR_LOOP, + EFFECT_NONE, + Light, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -28,13 +35,16 @@ from homeassistant.components.light import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util.color import color_hs_to_xy -from .const import DOMAIN as DECONZ_DOMAIN, NEW_GROUP, NEW_LIGHT, POWER_PLUGS +from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry DECONZ_GROUP = "is_deconz_group" +EFFECT_TO_DECONZ = {EFFECT_COLORLOOP: EFFECT_COLOR_LOOP, "None": EFFECT_NONE} +FLASH_TO_DECONZ = {FLASH_SHORT: ALERT_SHORT, FLASH_LONG: ALERT_LONG} async def async_setup_entry(hass, config_entry, async_add_entities): @@ -60,7 +70,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_light + hass, + gateway.signal_new_light, + async_add_light, ) ) @@ -86,7 +98,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_GROUP), async_add_group + hass, + gateway.signal_new_group, + async_add_group, ) ) @@ -126,6 +140,7 @@ class DeconzBaseLight(DeconzDevice, LightEntity): if device.effect is not None: self._attr_supported_features |= SUPPORT_EFFECT + self._attr_effect_list = [EFFECT_COLORLOOP] @property def color_mode(self) -> str: @@ -147,11 +162,6 @@ class DeconzBaseLight(DeconzDevice, LightEntity): """Return the brightness of this light between 0..255.""" return self._device.brightness - @property - def effect_list(self): - """Return the list of supported effects.""" - return [EFFECT_COLORLOOP] - @property def color_temp(self): """Return the CT color value.""" @@ -197,19 +207,12 @@ class DeconzBaseLight(DeconzDevice, LightEntity): elif "IKEA" in self._device.manufacturer: data["transition_time"] = 0 - if ATTR_FLASH in kwargs: - if kwargs[ATTR_FLASH] == FLASH_SHORT: - data["alert"] = "select" - del data["on"] - elif kwargs[ATTR_FLASH] == FLASH_LONG: - data["alert"] = "lselect" - del data["on"] + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: + data["alert"] = alert + del data["on"] - if ATTR_EFFECT in kwargs: - if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: - data["effect"] = "colorloop" - else: - data["effect"] = "none" + if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT))) is not None: + data["effect"] = effect await self._device.set_state(**data) @@ -224,20 +227,16 @@ class DeconzBaseLight(DeconzDevice, LightEntity): data["brightness"] = 0 data["transition_time"] = int(kwargs[ATTR_TRANSITION] * 10) - if ATTR_FLASH in kwargs: - if kwargs[ATTR_FLASH] == FLASH_SHORT: - data["alert"] = "select" - del data["on"] - elif kwargs[ATTR_FLASH] == FLASH_LONG: - data["alert"] = "lselect" - del data["on"] + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: + data["alert"] = alert + del data["on"] await self._device.set_state(**data) @property def extra_state_attributes(self): """Return the device state attributes.""" - return {DECONZ_GROUP: self._device.type == "LightGroup"} + return {DECONZ_GROUP: isinstance(self._device, Group)} class DeconzLight(DeconzBaseLight): @@ -268,15 +267,15 @@ class DeconzGroup(DeconzBaseLight): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return { - "identifiers": {(DECONZ_DOMAIN, self.unique_id)}, - "manufacturer": "Dresden Elektronik", - "model": "deCONZ group", - "name": self._device.name, - "via_device": (DECONZ_DOMAIN, self.gateway.api.config.bridge_id), - } + return DeviceInfo( + identifiers={(DECONZ_DOMAIN, self.unique_id)}, + manufacturer="Dresden Elektronik", + model="deCONZ group", + name=self._device.name, + via_device=(DECONZ_DOMAIN, self.gateway.api.config.bridge_id), + ) @property def extra_state_attributes(self): diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index bb23ec4be7a..fb344e54176 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -7,7 +7,6 @@ from homeassistant.components.lock import DOMAIN, LockEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import NEW_LIGHT, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -35,7 +34,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock_from_light + hass, + gateway.signal_new_light, + async_add_lock_from_light, ) ) @@ -58,7 +59,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( hass, - gateway.async_signal_new_device(NEW_SENSOR), + gateway.signal_new_sensor, async_add_lock_from_sensor, ) ) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index a3dae8f5470..68b89b70b9c 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ - "pydeconz==84" + "pydeconz==85" ], "ssdp": [ { diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py new file mode 100644 index 00000000000..0ac355f7dd1 --- /dev/null +++ b/homeassistant/components/deconz/number.py @@ -0,0 +1,126 @@ +"""Support for configuring different deCONZ sensors.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from pydeconz.sensor import PRESENCE_DELAY, Presence + +from homeassistant.components.number import ( + DOMAIN, + NumberEntity, + NumberEntityDescription, +) +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .deconz_device import DeconzDevice +from .gateway import get_gateway_from_config_entry + + +@dataclass +class DeconzNumberEntityDescription(NumberEntityDescription): + """Class describing deCONZ number entities.""" + + entity_category = ENTITY_CATEGORY_CONFIG + device_property: str | None = None + suffix: str | None = None + update_key: str | None = None + max_value: int | None = None + min_value: int | None = None + step: int | None = None + + +ENTITY_DESCRIPTIONS = { + Presence: [ + DeconzNumberEntityDescription( + key="delay", + device_property="delay", + suffix="Delay", + update_key=PRESENCE_DELAY, + max_value=65535, + min_value=0, + step=1, + ) + ] +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the deCONZ number entity.""" + gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() + + @callback + def async_add_sensor(sensors=gateway.api.sensors.values()): + """Add number config sensor from deCONZ.""" + entities = [] + + for sensor in sensors: + + if sensor.type.startswith("CLIP"): + continue + + known_number_entities = set(gateway.entities[DOMAIN]) + for description in ENTITY_DESCRIPTIONS.get(type(sensor), []): + + if getattr(sensor, description.device_property) is None: + continue + + new_number_entity = DeconzNumber(sensor, gateway, description) + if new_number_entity.unique_id not in known_number_entities: + entities.append(new_number_entity) + + if entities: + async_add_entities(entities) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + gateway.signal_new_sensor, + async_add_sensor, + ) + ) + + async_add_sensor( + [gateway.api.sensors[key] for key in sorted(gateway.api.sensors, key=int)] + ) + + +class DeconzNumber(DeconzDevice, NumberEntity): + """Representation of a deCONZ number entity.""" + + TYPE = DOMAIN + + def __init__(self, device, gateway, description): + """Initialize deCONZ number entity.""" + self.entity_description = description + super().__init__(device, gateway) + + self._attr_name = f"{self._device.name} {description.suffix}" + self._attr_max_value = description.max_value + self._attr_min_value = description.min_value + self._attr_step = description.step + + @callback + def async_update_callback(self) -> None: + """Update the number value.""" + keys = {self.entity_description.update_key, "reachable"} + if self._device.changed_keys.intersection(keys): + super().async_update_callback() + + @property + def value(self) -> float: + """Return the value of the sensor property.""" + return getattr(self._device, self.entity_description.device_property) + + async def async_set_value(self, value: float) -> None: + """Set sensor config.""" + data = {self.entity_description.device_property: int(value)} + await self._device.set_config(**data) + + @property + def unique_id(self) -> str: + """Return a unique identifier for this entity.""" + return f"{self.serial}-{self.entity_description.suffix.lower()}" diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 45e891add28..69f3d48c82c 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -5,7 +5,6 @@ from homeassistant.components.scene import Scene from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import NEW_SCENE from .gateway import get_gateway_from_config_entry @@ -23,7 +22,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_SCENE), async_add_scene + hass, + gateway.signal_new_scene, + async_add_scene, ) ) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 8b82c2fa7bf..3f8c22d43d6 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -33,6 +33,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -45,7 +46,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) -from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR +from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -73,6 +74,7 @@ ENTITY_DESCRIPTIONS = { device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=PERCENTAGE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), Consumption: SensorEntityDescription( key="consumption", @@ -167,7 +169,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor + hass, + gateway.signal_new_sensor, + async_add_sensor, ) ) @@ -189,11 +193,11 @@ class DeconzSensor(DeconzDevice, SensorEntity): self.entity_description = entity_description @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self): """Update the sensor's state.""" keys = {"on", "reachable", "state"} - if force_update or self._device.changed_keys.intersection(keys): - super().async_update_callback(force_update=force_update) + if self._device.changed_keys.intersection(keys): + super().async_update_callback() @property def native_value(self): @@ -211,13 +215,13 @@ class DeconzSensor(DeconzDevice, SensorEntity): if self._device.secondary_temperature is not None: attr[ATTR_TEMPERATURE] = self._device.secondary_temperature - if self._device.type in Consumption.ZHATYPE: + if isinstance(self._device, Consumption): attr[ATTR_POWER] = self._device.power - elif self._device.type in Daylight.ZHATYPE: + elif isinstance(self._device, Daylight): attr[ATTR_DAYLIGHT] = self._device.daylight - elif self._device.type in LightLevel.ZHATYPE: + elif isinstance(self._device, LightLevel): if self._device.dark is not None: attr[ATTR_DARK] = self._device.dark @@ -225,7 +229,7 @@ class DeconzSensor(DeconzDevice, SensorEntity): if self._device.daylight is not None: attr[ATTR_DAYLIGHT] = self._device.daylight - elif self._device.type in Power.ZHATYPE: + elif isinstance(self._device, Power): attr[ATTR_CURRENT] = self._device.current attr[ATTR_VOLTAGE] = self._device.voltage @@ -253,11 +257,11 @@ class DeconzTemperature(DeconzDevice, SensorEntity): return f"{self.serial}-temperature" @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self): """Update the sensor's state.""" keys = {"temperature", "reachable"} - if force_update or self._device.changed_keys.intersection(keys): - super().async_update_callback(force_update=force_update) + if self._device.changed_keys.intersection(keys): + super().async_update_callback() @property def native_value(self): @@ -278,11 +282,11 @@ class DeconzBattery(DeconzDevice, SensorEntity): self._attr_name = f"{self._device.name} Battery Level" @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self): """Update the battery's state, if needed.""" keys = {"battery", "reachable"} - if force_update or self._device.changed_keys.intersection(keys): - super().async_update_callback(force_update=force_update) + if self._device.changed_keys.intersection(keys): + super().async_update_callback() @property def unique_id(self): @@ -335,12 +339,12 @@ class DeconzSensorStateTracker: self.sensor = None @callback - def async_update_callback(self, ignore_update=False): + def async_update_callback(self): """Sensor state updated.""" if "battery" in self.sensor.changed_keys: async_dispatcher_send( self.gateway.hass, - self.gateway.async_signal_new_device(NEW_SENSOR), + self.gateway.signal_new_sensor, [self.sensor], ) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 361ab1715c0..535dd9807fb 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -1,11 +1,14 @@ """deCONZ services.""" -import asyncio - from pydeconz.utils import normalize_bridge_id import voluptuous as vol -from homeassistant.helpers import config_validation as cv +from homeassistant.core import callback +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity_registry import ( async_entries_for_config_entry, @@ -13,15 +16,7 @@ from homeassistant.helpers.entity_registry import ( ) from .config_flow import get_master_gateway -from .const import ( - CONF_BRIDGE_ID, - DOMAIN, - LOGGER, - NEW_GROUP, - NEW_LIGHT, - NEW_SCENE, - NEW_SENSOR, -) +from .const import CONF_BRIDGE_ID, DOMAIN, LOGGER DECONZ_SERVICES = "deconz_services" @@ -46,13 +41,22 @@ SERVICE_DEVICE_REFRESH = "device_refresh" SERVICE_REMOVE_ORPHANED_ENTRIES = "remove_orphaned_entries" SELECT_GATEWAY_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGE_ID): str})) +SUPPORTED_SERVICES = ( + SERVICE_CONFIGURE_DEVICE, + SERVICE_DEVICE_REFRESH, + SERVICE_REMOVE_ORPHANED_ENTRIES, +) -async def async_setup_services(hass): +SERVICE_TO_SCHEMA = { + SERVICE_CONFIGURE_DEVICE: SERVICE_CONFIGURE_DEVICE_SCHEMA, + SERVICE_DEVICE_REFRESH: SELECT_GATEWAY_SCHEMA, + SERVICE_REMOVE_ORPHANED_ENTRIES: SELECT_GATEWAY_SCHEMA, +} + + +@callback +def async_setup_services(hass): """Set up services for deCONZ integration.""" - if hass.data.get(DECONZ_SERVICES, False): - return - - hass.data[DECONZ_SERVICES] = True async def async_call_deconz_service(service_call): """Call correct deCONZ service.""" @@ -83,38 +87,20 @@ async def async_setup_services(hass): elif service == SERVICE_REMOVE_ORPHANED_ENTRIES: await async_remove_orphaned_entries_service(gateway) - hass.services.async_register( - DOMAIN, - SERVICE_CONFIGURE_DEVICE, - async_call_deconz_service, - schema=SERVICE_CONFIGURE_DEVICE_SCHEMA, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_DEVICE_REFRESH, - async_call_deconz_service, - schema=SELECT_GATEWAY_SCHEMA, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_REMOVE_ORPHANED_ENTRIES, - async_call_deconz_service, - schema=SELECT_GATEWAY_SCHEMA, - ) + for service in SUPPORTED_SERVICES: + hass.services.async_register( + DOMAIN, + service, + async_call_deconz_service, + schema=SERVICE_TO_SCHEMA[service], + ) -async def async_unload_services(hass): +@callback +def async_unload_services(hass): """Unload deCONZ services.""" - if not hass.data.get(DECONZ_SERVICES): - return - - hass.data[DECONZ_SERVICES] = False - - hass.services.async_remove(DOMAIN, SERVICE_CONFIGURE_DEVICE) - hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) - hass.services.async_remove(DOMAIN, SERVICE_REMOVE_ORPHANED_ENTRIES) + for service in SUPPORTED_SERVICES: + hass.services.async_remove(DOMAIN, service) async def async_configure_service(gateway, data): @@ -153,16 +139,14 @@ async def async_refresh_devices_service(gateway): await gateway.api.refresh_state() gateway.ignore_state_updates = False - for new_device_type in (NEW_GROUP, NEW_LIGHT, NEW_SCENE, NEW_SENSOR): - gateway.async_add_device_callback(new_device_type, force=True) + for resource_type in gateway.deconz_resource_type_to_signal_new_device: + gateway.async_add_device_callback(resource_type, force=True) async def async_remove_orphaned_entries_service(gateway): """Remove orphaned deCONZ entries from device and entity registries.""" - device_registry, entity_registry = await asyncio.gather( - gateway.hass.helpers.device_registry.async_get_registry(), - gateway.hass.helpers.entity_registry.async_get_registry(), - ) + device_registry = dr.async_get(gateway.hass) + entity_registry = er.async_get(gateway.hass) entity_entries = async_entries_for_config_entry( entity_registry, gateway.config_entry.entry_id diff --git a/homeassistant/components/deconz/siren.py b/homeassistant/components/deconz/siren.py index 9138bb3ac14..c3679b6ad89 100644 --- a/homeassistant/components/deconz/siren.py +++ b/homeassistant/components/deconz/siren.py @@ -13,7 +13,6 @@ from homeassistant.components.siren import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import NEW_LIGHT from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -41,7 +40,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_siren + hass, + gateway.signal_new_light, + async_add_siren, ) ) diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index f7559e37838..39489fe1fc3 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -4,9 +4,10 @@ from pydeconz.light import Siren from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.core import callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DOMAIN as DECONZ_DOMAIN, NEW_LIGHT, POWER_PLUGS +from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -19,7 +20,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) # Siren platform replacing sirens in switch platform added in 2021.10 for light in gateway.api.lights.values(): @@ -48,7 +49,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_switch + hass, + gateway.signal_new_light, + async_add_switch, ) ) diff --git a/homeassistant/components/deconz/translations/bg.json b/homeassistant/components/deconz/translations/bg.json index 24e36ecbe55..3fe700efd3e 100644 --- a/homeassistant/components/deconz/translations/bg.json +++ b/homeassistant/components/deconz/translations/bg.json @@ -19,6 +19,12 @@ "link": { "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 deCONZ Settings -> Gateway -> Advanced\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Authenticate app\"", "title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ" + }, + "manual_input": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } } } }, @@ -73,7 +79,8 @@ "deconz_devices": { "data": { "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ CLIP \u0441\u0435\u043d\u0437\u043e\u0440\u0438", - "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 deCONZ \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u043d\u0438 \u0433\u0440\u0443\u043f\u0438" + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 deCONZ \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u043d\u0438 \u0433\u0440\u0443\u043f\u0438", + "allow_new_devices": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u044f\u043d\u0435 \u043d\u0430 \u043d\u043e\u0432\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 \u0442\u0438\u043f\u043e\u0432\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 deCONZ" } diff --git a/homeassistant/components/deconz/translations/he.json b/homeassistant/components/deconz/translations/he.json index 74a8e1ba54b..3e2b350a0d9 100644 --- a/homeassistant/components/deconz/translations/he.json +++ b/homeassistant/components/deconz/translations/he.json @@ -30,10 +30,23 @@ }, "device_automation": { "trigger_subtype": { + "both_buttons": "\u05e9\u05e0\u05d9 \u05d4\u05dc\u05d7\u05e6\u05e0\u05d9\u05dd", + "button_1": "\u05dc\u05d7\u05e6\u05df \u05e8\u05d0\u05e9\u05d5\u05df", + "button_2": "\u05dc\u05d7\u05e6\u05df \u05e9\u05e0\u05d9", + "button_3": "\u05dc\u05d7\u05e6\u05df \u05e9\u05dc\u05d9\u05e9\u05d9", + "button_4": "\u05dc\u05d7\u05e6\u05df \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", "button_7": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05d1\u05d9\u05e2\u05d9", - "button_8": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05de\u05d9\u05e0\u05d9" + "button_8": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05de\u05d9\u05e0\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\u05d9\u05d1\u05d5\u05d9", + "turn_on": "\u05d4\u05e4\u05e2\u05dc\u05d4" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index a78a6ef1961..c71689a555d 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "no_bridges": "Nem tal\u00e1lhat\u00f3 deCONZ \u00e1tj\u00e1r\u00f3", "no_hardware_available": "Nincs deCONZ-hoz csatlakoztatott r\u00e1di\u00f3hardver", "not_deconz_bridge": "Nem egy deCONZ \u00e1tj\u00e1r\u00f3", @@ -71,7 +71,7 @@ "remote_button_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak", "remote_button_rotated": "A gomb elforgatva: \"{subtype}\"", "remote_button_rotated_fast": "A gomb gyorsan elfordult: \"{subtype}\"", - "remote_button_rotation_stopped": "A(z) \"{subtype}\" gomb forg\u00e1sa le\u00e1llt", + "remote_button_rotation_stopped": "\"{subtype}\" gomb forg\u00e1sa le\u00e1llt", "remote_button_short_press": "\"{subtype}\" gomb lenyomva", "remote_button_short_release": "\"{subtype}\" gomb elengedve", "remote_button_triple_press": "\"{subtype}\" gombra h\u00e1romszor kattintottak", diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index 240e04262e4..be03f3b2036 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -1,7 +1,15 @@ { "config": { + "abort": { + "already_configured": "\u30d6\u30ea\u30c3\u30b8\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "no_key": "API\u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + }, + "step": { + "link": { + "title": "deCONZ\u3068\u30ea\u30f3\u30af\u3059\u308b" + } } } } \ No newline at end of file diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index 2aff1b5266c..bc94b7ad014 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -68,11 +68,6 @@ class DelugeSwitch(ToggleEntity): """Return the name of the switch.""" return self._name - @property - def state(self): - """Return the state of the device.""" - return self._state - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index c4186eae505..7cf96d33f5c 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -4,6 +4,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, BinarySensorEntity, ) +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN @@ -38,15 +39,15 @@ class DemoBinarySensor(BinarySensorEntity): self._sensor_type = device_class @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers are unique identifiers within a specific domain (DOMAIN, self.unique_id) }, - "name": self.name, - } + name=self.name, + ) @property def unique_id(self): diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index a6d166662ae..ff8e0c256c6 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -20,6 +20,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN @@ -154,15 +155,15 @@ class DemoClimate(ClimateEntity): self._target_temperature_low = target_temp_low @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers are unique identifiers within a specific domain (DOMAIN, self.unique_id) }, - "name": self.name, - } + name=self.name, + ) @property def unique_id(self): diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 35f25df5a96..444b6a2a90c 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -11,6 +11,7 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_utc_time_change from . import DOMAIN @@ -86,15 +87,15 @@ class DemoCover(CoverEntity): self._closed = self.current_cover_position <= 0 @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers are unique identifiers within a specific domain (DOMAIN, self.unique_id) }, - "name": self.name, - } + name=self.name, + ) @property def unique_id(self): diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index 4d9496ca10f..29e7e1395ee 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -19,6 +19,7 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, LightEntity, ) +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN @@ -138,15 +139,15 @@ class DemoLight(LightEntity): self._features |= SUPPORT_EFFECT @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers are unique identifiers within a specific domain (DOMAIN, self.unique_id) }, - "name": self.name, - } + name=self.name, + ) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index cad2255806e..d471bdac85f 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -1,8 +1,12 @@ """Demo platform that offers a fake Number entity.""" from __future__ import annotations +from typing import Literal + from homeassistant.components.number import NumberEntity +from homeassistant.components.number.const import MODE_AUTO, MODE_BOX, MODE_SLIDER from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN @@ -17,6 +21,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= 42.0, "mdi:volume-high", False, + mode=MODE_SLIDER, ), DemoNumber( "pwm1", @@ -27,6 +32,27 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= 0.0, 1.0, 0.01, + MODE_BOX, + ), + DemoNumber( + "large_range", + "Large Range", + 500, + "mdi:square-wave", + False, + 1, + 1000, + 1, + ), + DemoNumber( + "small_range", + "Small Range", + 128, + "mdi:square-wave", + False, + 1, + 255, + 1, ), ] ) @@ -51,7 +77,8 @@ class DemoNumber(NumberEntity): assumed: bool, min_value: float | None = None, max_value: float | None = None, - step=None, + step: float | None = None, + mode: Literal["auto", "box", "slider"] = MODE_AUTO, ) -> None: """Initialize the Demo Number entity.""" self._attr_assumed_state = assumed @@ -59,6 +86,7 @@ class DemoNumber(NumberEntity): self._attr_name = name or DEVICE_DEFAULT_NAME self._attr_unique_id = unique_id self._attr_value = state + self._attr_mode = mode if min_value is not None: self._attr_min_value = min_value @@ -68,15 +96,15 @@ class DemoNumber(NumberEntity): self._attr_step = step @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers are unique identifiers within a specific domain (DOMAIN, self.unique_id) }, - "name": self.name, - } + name=self.name, + ) async def async_set_value(self, value): """Update the current value.""" diff --git a/homeassistant/components/demo/select.py b/homeassistant/components/demo/select.py index 8d499c7a258..6a768f80ba7 100644 --- a/homeassistant/components/demo/select.py +++ b/homeassistant/components/demo/select.py @@ -5,6 +5,7 @@ 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 import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -66,10 +67,10 @@ class DemoSelect(SelectEntity): self._attr_icon = icon self._attr_device_class = device_class self._attr_options = options - self._attr_device_info = { - "identifiers": {(DOMAIN, unique_id)}, - "name": name, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=name, + ) async def async_select_option(self, option: str) -> None: """Update the current selected option.""" diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 21ec8e1d391..413017ad2f1 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, StateType @@ -125,10 +126,10 @@ class DemoSensor(SensorEntity): self._attr_state_class = state_class self._attr_unique_id = unique_id - self._attr_device_info = { - "identifiers": {(DOMAIN, unique_id)}, - "name": name, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=name, + ) if battery: self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: battery} diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 84554bf0db1..dd969010bd7 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.components.switch import SwitchEntity from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN @@ -52,12 +53,12 @@ class DemoSwitch(SwitchEntity): self._attr_unique_id = unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + name=self.name, + ) def turn_on(self, **kwargs): """Turn the switch on.""" diff --git a/homeassistant/components/demo/translations/he.json b/homeassistant/components/demo/translations/he.json index c3162b87a5e..7e3349d3abc 100644 --- a/homeassistant/components/demo/translations/he.json +++ b/homeassistant/components/demo/translations/he.json @@ -1,3 +1,21 @@ { + "options": { + "step": { + "options_1": { + "data": { + "bool": "\u05d1\u05d5\u05dc\u05d9\u05d0\u05e0\u05d9 \u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9", + "constant": "\u05e7\u05d1\u05d5\u05e2", + "int": "\u05e7\u05dc\u05d8 \u05de\u05e1\u05e4\u05e8\u05d9" + } + }, + "options_2": { + "data": { + "multi": "\u05d1\u05d7\u05d9\u05e8\u05d4 \u05de\u05e8\u05d5\u05d1\u05d4", + "select": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea", + "string": "\u05e2\u05e8\u05da \u05de\u05d7\u05e8\u05d5\u05d6\u05ea" + } + } + } + }, "title": "\u05d4\u05d3\u05d2\u05de\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/select.he.json b/homeassistant/components/demo/translations/select.he.json index 0264a0021e2..bb4bb95d3d5 100644 --- a/homeassistant/components/demo/translations/select.he.json +++ b/homeassistant/components/demo/translations/select.he.json @@ -1,7 +1,9 @@ { "state": { "demo__speed": { - "light_speed": "\u05de\u05d4\u05d9\u05e8\u05d5\u05ea \u05d4\u05d0\u05d5\u05e8" + "light_speed": "\u05de\u05d4\u05d9\u05e8\u05d5\u05ea \u05d4\u05d0\u05d5\u05e8", + "ludicrous_speed": "\u05de\u05d4\u05d9\u05e8\u05d5\u05ea \u05de\u05d2\u05d5\u05d7\u05db\u05ea", + "ridiculous_speed": "\u05de\u05d4\u05d9\u05e8\u05d5\u05ea \u05de\u05d2\u05d5\u05d7\u05db\u05ea" } } } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/zh-Hans.json b/homeassistant/components/demo/translations/zh-Hans.json index 9155b5066c5..1c40afabb6e 100644 --- a/homeassistant/components/demo/translations/zh-Hans.json +++ b/homeassistant/components/demo/translations/zh-Hans.json @@ -4,6 +4,7 @@ "options_1": { "data": { "bool": "\u5e03\u5c14\u9009\u9879", + "constant": "\u5e38\u91cf", "int": "\u6570\u503c\u8f93\u5165" } }, @@ -15,5 +16,6 @@ } } } - } + }, + "title": "\u6f14\u793a" } \ No newline at end of file diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index 1005858e729..ffb73327d31 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -111,8 +111,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: # check if IP address is set manually - host = user_input.get(CONF_HOST) - if host: + if host := user_input.get(CONF_HOST): self.host = host return await self.async_step_connect() diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index ce79d937264..1eb4cef9d85 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", "requirements": ["denonavr==0.10.9"], - "codeowners": ["@scarface-4711", "@starkillerOG"], + "codeowners": ["@ol-iver", "@starkillerOG"], "ssdp": [ { "manufacturer": "Denon", diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 68a5b8c71d8..39186680e09 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -35,9 +35,10 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import ATTR_COMMAND, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import ATTR_COMMAND, CONF_HOST, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import DeviceInfo from . import CONF_RECEIVER from .config_flow import ( @@ -144,9 +145,19 @@ class DenonDevice(MediaPlayerEntity): update_audyssey: bool, ) -> None: """Initialize the device.""" + self._attr_name = receiver.name + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + configuration_url=f"http://{config_entry.data[CONF_HOST]}/", + identifiers={(DOMAIN, config_entry.unique_id)}, + manufacturer=config_entry.data[CONF_MANUFACTURER], + model=f"{config_entry.data[CONF_MODEL]}-{config_entry.data[CONF_TYPE]}", + name=config_entry.title, + ) + self._attr_sound_mode_list = receiver.sound_mode_list + self._attr_source_list = receiver.input_func_list + self._receiver = receiver - self._unique_id = unique_id - self._config_entry = config_entry self._update_audyssey = update_audyssey self._supported_features_base = SUPPORT_DENON @@ -230,31 +241,6 @@ class DenonDevice(MediaPlayerEntity): """Return True if entity is available.""" return self._available - @property - def unique_id(self): - """Return the unique id of the zone.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info of the receiver.""" - if self._config_entry.data[CONF_SERIAL_NUMBER] is None: - return None - - device_info = { - "identifiers": {(DOMAIN, self._config_entry.unique_id)}, - "manufacturer": self._config_entry.data[CONF_MANUFACTURER], - "name": self._config_entry.title, - "model": f"{self._config_entry.data[CONF_MODEL]}-{self._config_entry.data[CONF_TYPE]}", - } - - return device_info - - @property - def name(self): - """Return the name of the device.""" - return self._receiver.name - @property def state(self): """Return the state of the device.""" @@ -279,21 +265,11 @@ class DenonDevice(MediaPlayerEntity): """Return the current input source.""" return self._receiver.input_func - @property - def source_list(self): - """Return a list of available input sources.""" - return self._receiver.input_func_list - @property def sound_mode(self): """Return the current matched sound mode.""" return self._receiver.sound_mode - @property - def sound_mode_list(self): - """Return a list of available sound modes.""" - return self._receiver.sound_mode_list - @property def supported_features(self): """Flag media player features that are supported.""" diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index c22d392dc8a..874f190ff01 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet", "not_denonavr_manufacturer": "Nem egy Denon AVR h\u00e1l\u00f3zati vev\u0151, felfedezett gy\u00e1rt\u00f3 nem egyezik", "not_denonavr_missing": "Nem Denon AVR h\u00e1l\u00f3zati vev\u0151, a felfedez\u00e9si inform\u00e1ci\u00f3k nem teljesek" diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 567e579d8b8..74582f0f77b 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -159,8 +159,7 @@ async def _async_get_device_automations( for device_id in match_device_ids: combined_results[device_id] = [] - device = device_registry.async_get(device_id) - if device is None: + if (device := device_registry.async_get(device_id)) is None: raise DeviceNotFound for entry_id in device.config_entries: if config_entry := hass.config_entries.async_get_entry(entry_id): @@ -221,8 +220,7 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom capabilities = capabilities.copy() - extra_fields = capabilities.get("extra_fields") - if extra_fields is None: + if (extra_fields := capabilities.get("extra_fields")) is None: capabilities["extra_fields"] = [] else: capabilities["extra_fields"] = voluptuous_serialize.convert( @@ -318,7 +316,9 @@ async def websocket_device_automation_get_action_capabilities(hass, connection, @websocket_api.websocket_command( { vol.Required("type"): "device_automation/condition/capabilities", - vol.Required("condition"): dict, + vol.Required("condition"): cv.DEVICE_CONDITION_BASE_SCHEMA.extend( + {}, extra=vol.ALLOW_EXTRA + ), } ) @websocket_api.async_response @@ -335,7 +335,9 @@ async def websocket_device_automation_get_condition_capabilities(hass, connectio @websocket_api.websocket_command( { vol.Required("type"): "device_automation/trigger/capabilities", - vol.Required("trigger"): dict, + vol.Required("trigger"): DEVICE_TRIGGER_BASE_SCHEMA.extend( + {}, extra=vol.ALLOW_EXTRA + ), } ) @websocket_api.async_response diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 5d08f8d9d31..99473777658 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -129,8 +129,7 @@ async def async_call_action_from_config( @callback def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" - condition_type = config[CONF_TYPE] - if condition_type == CONF_IS_ON: + if config[CONF_TYPE] == CONF_IS_ON: stat = "on" else: stat = "off" @@ -152,8 +151,7 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_type = config[CONF_TYPE] - if trigger_type == CONF_TURNED_ON: + if config[CONF_TYPE] == CONF_TURNED_ON: to_state = "on" else: to_state = "off" diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 31d060200f0..94638c031a3 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -396,8 +396,7 @@ async def get_tracker(hass: HomeAssistant, config: ConfigType) -> DeviceTracker: consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME) defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {}) - track_new = conf.get(CONF_TRACK_NEW) - if track_new is None: + if (track_new := conf.get(CONF_TRACK_NEW)) is None: track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) devices = await async_load_config(yaml_path, hass, consider_home) @@ -492,8 +491,7 @@ class DeviceTracker: raise HomeAssistantError("Neither mac or device id passed in") if mac is not None: mac = str(mac).upper() - device = self.mac_to_dev.get(mac) - if device is None: + if (device := self.mac_to_dev.get(mac)) is None: dev_id = util.slugify(host_name or "") or util.slugify(mac) else: dev_id = cv.slug(str(dev_id).lower()) diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index 03f850579be..f4f2432aa6e 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -2,11 +2,12 @@ from __future__ import annotations import logging +from urllib.parse import urlparse from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN from .subscriber import Subscriber @@ -32,15 +33,16 @@ class DevoloDeviceEntity(Entity): ].name self._attr_should_poll = False self._attr_unique_id = element_uid - self._attr_device_info = { - "identifiers": {(DOMAIN, self._device_instance.uid)}, - "name": self._attr_name, - "manufacturer": device_instance.brand, - "model": device_instance.name, - "suggested_area": device_instance.settings_property[ + self._attr_device_info = DeviceInfo( + configuration_url=f"https://{urlparse(device_instance.href).netloc}", + identifiers={(DOMAIN, self._device_instance.uid)}, + manufacturer=device_instance.brand, + model=device_instance.name, + name=self._attr_name, + suggested_area=device_instance.settings_property[ "general_device_settings" ].zone, - } + ) self.subscriber: Subscriber | None = None self.sync_callback = self._sync diff --git a/homeassistant/components/devolo_home_control/translations/bg.json b/homeassistant/components/devolo_home_control/translations/bg.json new file mode 100644 index 00000000000..d5f922c14ff --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index 68622a23350..8db69b38927 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -71,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/dexcom/translations/bg.json b/homeassistant/components/dexcom/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/dexcom/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 3e4fd8fec01..d52b30ccfb2 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -1,6 +1,5 @@ """The dhcp integration.""" -from abc import abstractmethod from datetime import timedelta import fnmatch from ipaddress import ip_address as make_ip_address @@ -17,6 +16,7 @@ from aiodiscover.discovery import ( from scapy.config import conf from scapy.error import Scapy_Exception +from homeassistant import config_entries from homeassistant.components.device_tracker.const import ( ATTR_HOST_NAME, ATTR_IP, @@ -31,6 +31,7 @@ from homeassistant.const import ( STATE_HOME, ) from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.helpers import discovery_flow from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.event import ( async_track_state_added_domain, @@ -38,10 +39,9 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_dhcp +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.network import is_invalid, is_link_local, is_loopback -from .const import DOMAIN - FILTER = "udp and (port 67 or 68)" REQUESTED_ADDR = "requested_addr" MESSAGE_TYPE = "message-type" @@ -89,6 +89,17 @@ class WatcherBase: self._address_data = address_data def process_client(self, ip_address, hostname, mac_address): + """Process a client.""" + return run_callback_threadsafe( + self.hass.loop, + self.async_process_client, + ip_address, + hostname, + mac_address, + ).result() + + @callback + def async_process_client(self, ip_address, hostname, mac_address): """Process a client.""" made_ip_address = make_ip_address(ip_address) @@ -101,7 +112,6 @@ class WatcherBase: return data = self._address_data.get(ip_address) - if ( data and data[MAC_ADDRESS] == mac_address @@ -111,12 +121,9 @@ class WatcherBase: # to process it return - self._address_data[ip_address] = {MAC_ADDRESS: mac_address, HOSTNAME: hostname} + data = {MAC_ADDRESS: mac_address, HOSTNAME: hostname} + self._address_data[ip_address] = data - self.process_updated_address_data(ip_address, self._address_data[ip_address]) - - def process_updated_address_data(self, ip_address, data): - """Process the address data update.""" lowercase_hostname = data[HOSTNAME].lower() uppercase_mac = data[MAC_ADDRESS].upper() @@ -139,23 +146,17 @@ class WatcherBase: continue _LOGGER.debug("Matched %s against %s", data, entry) - - self.create_task( - self.hass.config_entries.flow.async_init( - entry["domain"], - context={"source": DOMAIN}, - data={ - IP_ADDRESS: ip_address, - HOSTNAME: lowercase_hostname, - MAC_ADDRESS: data[MAC_ADDRESS], - }, - ) + discovery_flow.async_create_flow( + self.hass, + entry["domain"], + {"source": config_entries.SOURCE_DHCP}, + { + IP_ADDRESS: ip_address, + HOSTNAME: lowercase_hostname, + MAC_ADDRESS: data[MAC_ADDRESS], + }, ) - @abstractmethod - def create_task(self, task): - """Pass a task to async_add_task based on which context we are in.""" - class NetworkWatcher(WatcherBase): """Class to query ptr records routers.""" @@ -189,21 +190,17 @@ class NetworkWatcher(WatcherBase): """Start a new discovery task if one is not running.""" if self._discover_task and not self._discover_task.done(): return - self._discover_task = self.create_task(self.async_discover()) + self._discover_task = self.hass.async_create_task(self.async_discover()) async def async_discover(self): """Process discovery.""" for host in await self._discover_hosts.async_discover(): - self.process_client( + self.async_process_client( host[DISCOVERY_IP_ADDRESS], host[DISCOVERY_HOSTNAME], _format_mac(host[DISCOVERY_MAC_ADDRESS]), ) - def create_task(self, task): - """Pass a task to async_create_task since we are in async context.""" - return self.hass.async_create_task(task) - class DeviceTrackerWatcher(WatcherBase): """Class to watch dhcp data from routers.""" @@ -250,11 +247,7 @@ class DeviceTrackerWatcher(WatcherBase): if ip_address is None or mac_address is None: return - self.process_client(ip_address, hostname, _format_mac(mac_address)) - - def create_task(self, task): - """Pass a task to async_create_task since we are in async context.""" - return self.hass.async_create_task(task) + self.async_process_client(ip_address, hostname, _format_mac(mac_address)) class DHCPWatcher(WatcherBase): @@ -353,10 +346,6 @@ class DHCPWatcher(WatcherBase): if self._sniffer.thread: self._sniffer.thread.name = self.__class__.__name__ - def create_task(self, task): - """Pass a task to hass.add_job since we are in a thread.""" - return self.hass.add_job(task) - def _decode_dhcp_option(dhcp_options, key): """Extract and decode data from a packet option.""" diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 8ec6bf855c8..312b83c3311 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,7 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.5", "aiodiscover==1.4.4"], + "requirements": ["scapy==2.4.5", "aiodiscover==1.4.5"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index 6003f17c9e9..9473fd537ad 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -106,8 +106,7 @@ async def async_handle_message(hass, message): "Dialogflow V1 API will be removed on October 23, 2019. Please change your DialogFlow settings to use the V2 api" ) req = message.get("result") - action_incomplete = req.get("actionIncomplete", True) - if action_incomplete: + if req.get("actionIncomplete", True): return elif _api_version is V2: diff --git a/homeassistant/components/dialogflow/translations/bg.json b/homeassistant/components/dialogflow/translations/bg.json index cc8faa1f0fd..d27bddfcd05 100644 --- a/homeassistant/components/dialogflow/translations/bg.json +++ b/homeassistant/components/dialogflow/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 [\u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 Dialogflow]({dialogflow_url}). \n\n \u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: ` {webhook_url} ` \n - Method: POST\n - Content Type: application/json\n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." }, diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index 9a9f82c36d2..b5188092862 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -36,8 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Digital Ocean droplet sensor.""" - digital = hass.data.get(DATA_DIGITAL_OCEAN) - if not digital: + if not (digital := hass.data.get(DATA_DIGITAL_OCEAN)): return False droplets = config[CONF_DROPLETS] diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index 0678b9ab1a1..d52c223c866 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -33,8 +33,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Digital Ocean droplet switch.""" - digital = hass.data.get(DATA_DIGITAL_OCEAN) - if not digital: + if not (digital := hass.data.get(DATA_DIGITAL_OCEAN)): return False droplets = config[CONF_DROPLETS] diff --git a/homeassistant/components/directv/const.py b/homeassistant/components/directv/const.py index b840b7bd2dc..e90fd6879c7 100644 --- a/homeassistant/components/directv/const.py +++ b/homeassistant/components/directv/const.py @@ -1,6 +1,4 @@ """Constants for the DirecTV integration.""" -from typing import Final - DOMAIN = "directv" # Attributes @@ -8,7 +6,6 @@ ATTR_MEDIA_CURRENTLY_RECORDING = "media_currently_recording" ATTR_MEDIA_RATING = "media_rating" ATTR_MEDIA_RECORDED = "media_recorded" ATTR_MEDIA_START_TIME = "media_start_time" -ATTR_VIA_DEVICE: Final = "via_device" CONF_RECEIVER_ID = "receiver_id" diff --git a/homeassistant/components/directv/entity.py b/homeassistant/components/directv/entity.py index 2e6ffb81a52..08b24a50a75 100644 --- a/homeassistant/components/directv/entity.py +++ b/homeassistant/components/directv/entity.py @@ -3,16 +3,9 @@ from __future__ import annotations from directv import DIRECTV -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, -) from homeassistant.helpers.entity import DeviceInfo, Entity -from .const import ATTR_VIA_DEVICE, DOMAIN +from .const import DOMAIN class DIRECTVEntity(Entity): @@ -28,11 +21,10 @@ class DIRECTVEntity(Entity): @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_SW_VERSION: self.dtv.device.info.version, - ATTR_VIA_DEVICE: (DOMAIN, self.dtv.device.info.receiver_id), - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + manufacturer=self.dtv.device.info.brand, + name=self.name, + sw_version=self.dtv.device.info.version, + via_device=(DOMAIN, self.dtv.device.info.receiver_id), + ) diff --git a/homeassistant/components/directv/translations/bg.json b/homeassistant/components/directv/translations/bg.json new file mode 100644 index 00000000000..ffb69776060 --- /dev/null +++ b/homeassistant/components/directv/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 6d3dc704d83..16f30fbf051 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -59,10 +59,22 @@ class DiscordNotificationService(BaseNotificationService): data = kwargs.get(ATTR_DATA) or {} + embed = None if ATTR_EMBED in data: embedding = data[ATTR_EMBED] fields = embedding.get(ATTR_EMBED_FIELDS) + if embedding: + embed = discord.Embed(**embedding) + for field in fields: + embed.add_field(**field) + if ATTR_EMBED_FOOTER in embedding: + embed.set_footer(**embedding[ATTR_EMBED_FOOTER]) + if ATTR_EMBED_AUTHOR in embedding: + embed.set_author(**embedding[ATTR_EMBED_AUTHOR]) + if ATTR_EMBED_THUMBNAIL in embedding: + embed.set_thumbnail(**embedding[ATTR_EMBED_THUMBNAIL]) + if ATTR_IMAGES in data: images = [] @@ -76,43 +88,21 @@ class DiscordNotificationService(BaseNotificationService): else: _LOGGER.warning("Image not found: %s", image) - # pylint: disable=unused-variable - @discord_bot.event - async def on_ready(): - """Send the messages when the bot is ready.""" - try: - for channelid in kwargs[ATTR_TARGET]: - channelid = int(channelid) - channel = discord_bot.get_channel( + await discord_bot.login(self.token) + + try: + for channelid in kwargs[ATTR_TARGET]: + channelid = int(channelid) + try: + channel = await discord_bot.fetch_channel( channelid - ) or discord_bot.get_user(channelid) - - if channel is None: - _LOGGER.warning("Channel not found for ID: %s", channelid) - continue - # Must create new instances of File for each channel. - files = None - if images: - files = [] - for image in images: - files.append(discord.File(image)) - if embedding: - embed = discord.Embed(**embedding) - if fields: - for field_num, field_name in enumerate(fields): - embed.add_field(**fields[field_num]) - if ATTR_EMBED_FOOTER in embedding: - embed.set_footer(**embedding[ATTR_EMBED_FOOTER]) - if ATTR_EMBED_AUTHOR in embedding: - embed.set_author(**embedding[ATTR_EMBED_AUTHOR]) - if ATTR_EMBED_THUMBNAIL in embedding: - embed.set_thumbnail(**embedding[ATTR_EMBED_THUMBNAIL]) - await channel.send(message, files=files, embed=embed) - else: - await channel.send(message, files=files) - except (discord.errors.HTTPException, discord.errors.NotFound) as error: - _LOGGER.warning("Communication error: %s", error) - await discord_bot.close() - - # Using reconnect=False prevents multiple ready events to be fired. - await discord_bot.start(self.token, reconnect=False) + ) or await discord_bot.fetch_user(channelid) + except discord.NotFound: + _LOGGER.warning("Channel not found for ID: %s", channelid) + continue + # Must create new instances of File for each channel. + files = [discord.File(image) for image in images] if images else None + await channel.send(message, files=files, embed=embed) + except (discord.HTTPException, discord.NotFound) as error: + _LOGGER.warning("Communication error: %s", error) + await discord_bot.close() diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index bade569bb46..595771cd673 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -38,7 +38,6 @@ SERVICE_SAMSUNG_PRINTER = "samsung_printer" SERVICE_TELLDUSLIVE = "tellstick" SERVICE_YEELIGHT = "yeelight" SERVICE_WEMO = "belkin_wemo" -SERVICE_WINK = "wink" SERVICE_XIAOMI_GW = "xiaomi_gw" # These have custom protocols @@ -94,7 +93,6 @@ MIGRATED_SERVICE_HANDLERS = [ "sonos", "songpal", SERVICE_WEMO, - SERVICE_WINK, SERVICE_XIAOMI_GW, "volumio", SERVICE_YEELIGHT, diff --git a/homeassistant/components/dlna_dmr/__init__.py b/homeassistant/components/dlna_dmr/__init__.py index 536567336fd..6a53490819f 100644 --- a/homeassistant/components/dlna_dmr/__init__.py +++ b/homeassistant/components/dlna_dmr/__init__.py @@ -3,39 +3,13 @@ from __future__ import annotations from homeassistant import config_entries from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN -from homeassistant.const import CONF_PLATFORM, CONF_URL from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, LOGGER +from .const import LOGGER PLATFORMS = [MEDIA_PLAYER_DOMAIN] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up DLNA component.""" - if MEDIA_PLAYER_DOMAIN not in config: - return True - - for entry_config in config[MEDIA_PLAYER_DOMAIN]: - if entry_config.get(CONF_PLATFORM) != DOMAIN: - continue - LOGGER.warning( - "Configuring dlna_dmr via yaml is deprecated; the configuration for" - " %s has been migrated to a config entry and can be safely removed", - entry_config.get(CONF_URL), - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=entry_config, - ) - ) - - return True - - async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 53513d593f5..8cd4f706087 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -14,7 +14,13 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, CONF_TYPE, CONF_URL +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_HOST, + CONF_NAME, + CONF_TYPE, + CONF_URL, +) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import IntegrationError @@ -25,6 +31,7 @@ from .const import ( CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, + DEFAULT_NAME, DOMAIN, ) from .data import get_domain_data @@ -50,7 +57,12 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" - self._discoveries: list[Mapping[str, str]] = [] + self._discoveries: dict[str, Mapping[str, Any]] = {} + self._location: str | None = None + self._udn: str | None = None + self._device_type: str | None = None + self._name: str | None = None + self._options: dict[str, Any] = {} @staticmethod @callback @@ -61,41 +73,68 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return DlnaDmrOptionsFlowHandler(config_entry) async def async_step_user(self, user_input: FlowInput = None) -> FlowResult: - """Handle a flow initialized by the user: manual URL entry. + """Handle a flow initialized by the user. - Discovered devices will already be displayed, no need to prompt user - with them here. + Let user choose from a list of found and unconfigured devices or to + enter an URL manually. """ LOGGER.debug("async_step_user: user_input: %s", user_input) + if user_input is not None: + host = user_input.get(CONF_HOST) + if not host: + # No device chosen, user might want to directly enter an URL + return await self.async_step_manual() + # User has chosen a device, ask for confirmation + discovery = self._discoveries[host] + await self._async_set_info_from_discovery(discovery) + return self._create_entry() + + discoveries = await self._async_get_discoveries() + if not discoveries: + # Nothing found, maybe the user knows an URL to try + return await self.async_step_manual() + + self._discoveries = { + discovery.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + or urlparse(discovery[ssdp.ATTR_SSDP_LOCATION]).hostname: discovery + for discovery in discoveries + } + + data_schema = vol.Schema( + {vol.Optional(CONF_HOST): vol.In(self._discoveries.keys())} + ) + return self.async_show_form(step_id="user", data_schema=data_schema) + + async def async_step_manual(self, user_input: FlowInput = None) -> FlowResult: + """Manual URL entry by the user.""" + LOGGER.debug("async_step_manual: user_input: %s", user_input) + + # Device setup manually, assume we don't get SSDP broadcast notifications + self._options[CONF_POLL_AVAILABILITY] = True + errors = {} if user_input is not None: + self._location = user_input[CONF_URL] try: - discovery = await self._async_connect(user_input[CONF_URL]) + await self._async_connect() except ConnectError as err: errors["base"] = err.args[0] else: - # If unmigrated config was imported earlier then use it - import_data = get_domain_data(self.hass).unmigrated_config.get( - user_input[CONF_URL] - ) - if import_data is not None: - return await self.async_step_import(import_data) - # Device setup manually, assume we don't get SSDP broadcast notifications - options = {CONF_POLL_AVAILABILITY: True} - return await self._async_create_entry_from_discovery(discovery, options) + return self._create_entry() data_schema = vol.Schema({CONF_URL: str}) return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors + step_id="manual", data_schema=data_schema, errors=errors ) async def async_step_import(self, import_data: FlowInput = None) -> FlowResult: """Import a new DLNA DMR device from a config entry. - This flow is triggered by `async_setup`. If no device has been - configured before, find any matching device and create a config_entry - for it. Otherwise, do nothing. + This flow is triggered by `async_setup_platform`. If the device has not + been migrated, and can be connected to, automatically import it. If it + cannot be connected to, prompt the user to turn it on. If it has already + been migrated, do nothing. """ LOGGER.debug("async_step_import: import_data: %s", import_data) @@ -103,130 +142,229 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug("Entry not imported: incomplete_config") return self.async_abort(reason="incomplete_config") - self._async_abort_entries_match({CONF_URL: import_data[CONF_URL]}) + self._location = import_data[CONF_URL] + self._async_abort_entries_match({CONF_URL: self._location}) - location = import_data[CONF_URL] - self._discoveries = await self._async_get_discoveries() - - poll_availability = True - - # Find the device in the list of unconfigured devices - for discovery in self._discoveries: - if discovery[ssdp.ATTR_SSDP_LOCATION] == location: - # Device found via SSDP, it shouldn't need polling - poll_availability = False - LOGGER.debug( - "Entry %s found via SSDP, with UDN %s", - import_data[CONF_URL], - discovery[ssdp.ATTR_SSDP_UDN], - ) - break - else: - # Not in discoveries. Try connecting directly. - try: - discovery = await self._async_connect(location) - except ConnectError as err: - LOGGER.debug( - "Entry %s not imported: %s", import_data[CONF_URL], err.args[0] - ) - # Store the config to apply if the device is added later - get_domain_data(self.hass).unmigrated_config[location] = import_data - return self.async_abort(reason=err.args[0]) + # Use the location as this config flow's unique ID until UDN is known + await self.async_set_unique_id(self._location) # Set options from the import_data, except listen_ip which is no longer used - options = { - CONF_LISTEN_PORT: import_data.get(CONF_LISTEN_PORT), - CONF_CALLBACK_URL_OVERRIDE: import_data.get(CONF_CALLBACK_URL_OVERRIDE), - CONF_POLL_AVAILABILITY: poll_availability, - } + self._options[CONF_LISTEN_PORT] = import_data.get(CONF_LISTEN_PORT) + self._options[CONF_CALLBACK_URL_OVERRIDE] = import_data.get( + CONF_CALLBACK_URL_OVERRIDE + ) # Override device name if it's set in the YAML - if CONF_NAME in import_data: - discovery = dict(discovery) - discovery[ssdp.ATTR_UPNP_FRIENDLY_NAME] = import_data[CONF_NAME] + self._name = import_data.get(CONF_NAME) - LOGGER.debug("Entry %s ready for import", import_data[CONF_URL]) - return await self._async_create_entry_from_discovery(discovery, options) + discoveries = await self._async_get_discoveries() + + # Find the device in the list of unconfigured devices + for discovery in discoveries: + if discovery[ssdp.ATTR_SSDP_LOCATION] == self._location: + # Device found via SSDP, it shouldn't need polling + self._options[CONF_POLL_AVAILABILITY] = False + # Discovery info has everything required to create config entry + await self._async_set_info_from_discovery(discovery) + LOGGER.debug( + "Entry %s found via SSDP, with UDN %s", + self._location, + self._udn, + ) + return self._create_entry() + + # This device will need to be polled + self._options[CONF_POLL_AVAILABILITY] = True + + # Device was not found via SSDP, connect directly for configuration + try: + await self._async_connect() + except ConnectError as err: + # This will require user action + LOGGER.debug("Entry %s not imported yet: %s", self._location, err.args[0]) + return await self.async_step_import_turn_on() + + LOGGER.debug("Entry %s ready for import", self._location) + return self._create_entry() + + async def async_step_import_turn_on( + self, user_input: FlowInput = None + ) -> FlowResult: + """Request the user to turn on the device so that import can finish.""" + LOGGER.debug("async_step_import_turn_on: %s", user_input) + + self.context["title_placeholders"] = {"name": self._name or self._location} + + errors = {} + if user_input is not None: + try: + await self._async_connect() + except ConnectError as err: + errors["base"] = err.args[0] + else: + return self._create_entry() + + self._set_confirm_only() + return self.async_show_form(step_id="import_turn_on", errors=errors) async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle a flow initialized by SSDP discovery.""" LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) - self._discoveries = [discovery_info] + await self._async_set_info_from_discovery(discovery_info) - udn = discovery_info[ssdp.ATTR_SSDP_UDN] - location = discovery_info[ssdp.ATTR_SSDP_LOCATION] + if _is_ignored_device(discovery_info): + return self.async_abort(reason="alternative_integration") - # Abort if already configured, but update the last-known location - await self.async_set_unique_id(udn) - self._abort_if_unique_id_configured( - updates={CONF_URL: location}, reload_on_update=False - ) + # Abort if the device doesn't support all services required for a DmrDevice. + # Use the discovery_info instead of DmrDevice.is_profile_device to avoid + # contacting the device again. + discovery_service_list = discovery_info.get(ssdp.ATTR_UPNP_SERVICE_LIST) + if not discovery_service_list: + return self.async_abort(reason="not_dmr") + discovery_service_ids = { + service.get("serviceId") + for service in discovery_service_list.get("service") or [] + } + if not DmrDevice.SERVICE_IDS.issubset(discovery_service_ids): + return self.async_abort(reason="not_dmr") - # If the device needs migration because it wasn't turned on when HA - # started, silently migrate it now. - import_data = get_domain_data(self.hass).unmigrated_config.get(location) - if import_data is not None: - return await self.async_step_import(import_data) + # Abort if a migration flow for the device's location is in progress + for progress in self._async_in_progress(include_uninitialized=True): + if progress["context"].get("unique_id") == self._location: + LOGGER.debug( + "Aborting SSDP setup because migration for %s is in progress", + self._location, + ) + return self.async_abort(reason="already_in_progress") - parsed_url = urlparse(location) - name = discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or parsed_url.hostname - self.context["title_placeholders"] = {"name": name} + self.context["title_placeholders"] = {"name": self._name} + + return await self.async_step_confirm() + + async def async_step_unignore(self, user_input: Mapping[str, Any]) -> FlowResult: + """Rediscover previously ignored devices by their unique_id.""" + LOGGER.debug("async_step_unignore: user_input: %s", user_input) + self._udn = user_input["unique_id"] + assert self._udn + await self.async_set_unique_id(self._udn) + + # Find a discovery matching the unignored unique_id for a DMR device + for dev_type in DmrDevice.DEVICE_TYPES: + discovery = await ssdp.async_get_discovery_info_by_udn_st( + self.hass, self._udn, dev_type + ) + if discovery: + break + else: + return self.async_abort(reason="discovery_error") + + await self._async_set_info_from_discovery(discovery, abort_if_configured=False) + + self.context["title_placeholders"] = {"name": self._name} return await self.async_step_confirm() async def async_step_confirm(self, user_input: FlowInput = None) -> FlowResult: - """Allow the user to confirm adding the device. - - Also check that the device is still available, otherwise when it is - added to HA it won't report the correct DeviceInfo. - """ + """Allow the user to confirm adding the device.""" LOGGER.debug("async_step_confirm: %s", user_input) - errors = {} if user_input is not None: - discovery = self._discoveries[0] - try: - await self._async_connect(discovery[ssdp.ATTR_SSDP_LOCATION]) - except ConnectError as err: - errors["base"] = err.args[0] - else: - return await self._async_create_entry_from_discovery(discovery) + return self._create_entry() self._set_confirm_only() - return self.async_show_form(step_id="confirm", errors=errors) + return self.async_show_form(step_id="confirm") - async def _async_create_entry_from_discovery( - self, - discovery: Mapping[str, Any], - options: Mapping[str, Any] | None = None, - ) -> FlowResult: - """Create an entry from discovery.""" - LOGGER.debug("_async_create_entry_from_discovery: discovery: %s", discovery) + async def _async_connect(self) -> None: + """Connect to a device to confirm it works and gather extra information. - location = discovery[ssdp.ATTR_SSDP_LOCATION] - udn = discovery[ssdp.ATTR_SSDP_UDN] + Updates this flow's unique ID to the device UDN if not already done. + Raises ConnectError if something goes wrong. + """ + LOGGER.debug("_async_connect: location: %s", self._location) + assert self._location, "self._location has not been set before connect" + + domain_data = get_domain_data(self.hass) + try: + device = await domain_data.upnp_factory.async_create_device(self._location) + except UpnpError as err: + raise ConnectError("cannot_connect") from err + + if not DmrDevice.is_profile_device(device): + raise ConnectError("not_dmr") + + device = find_device_of_type(device, DmrDevice.DEVICE_TYPES) + + if not self._udn: + self._udn = device.udn + await self.async_set_unique_id(self._udn) # Abort if already configured, but update the last-known location - await self.async_set_unique_id(udn) - self._abort_if_unique_id_configured(updates={CONF_URL: location}) + self._abort_if_unique_id_configured( + updates={CONF_URL: self._location}, reload_on_update=False + ) - parsed_url = urlparse(location) - title = discovery.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or parsed_url.hostname + if not self._device_type: + self._device_type = device.device_type + if not self._name: + self._name = device.name + + def _create_entry(self) -> FlowResult: + """Create a config entry, assuming all required information is now known.""" + LOGGER.debug( + "_async_create_entry: location: %s, UDN: %s", self._location, self._udn + ) + assert self._location + assert self._udn + assert self._device_type + + title = self._name or urlparse(self._location).hostname or DEFAULT_NAME data = { - CONF_URL: discovery[ssdp.ATTR_SSDP_LOCATION], - CONF_DEVICE_ID: discovery[ssdp.ATTR_SSDP_UDN], - CONF_TYPE: discovery.get(ssdp.ATTR_SSDP_NT) or discovery[ssdp.ATTR_SSDP_ST], + CONF_URL: self._location, + CONF_DEVICE_ID: self._udn, + CONF_TYPE: self._device_type, } - return self.async_create_entry(title=title, data=data, options=options) + return self.async_create_entry(title=title, data=data, options=self._options) - async def _async_get_discoveries(self) -> list[Mapping[str, str]]: + async def _async_set_info_from_discovery( + self, discovery_info: Mapping[str, Any], abort_if_configured: bool = True + ) -> None: + """Set information required for a config entry from the SSDP discovery.""" + LOGGER.debug( + "_async_set_info_from_discovery: location: %s, UDN: %s", + discovery_info[ssdp.ATTR_SSDP_LOCATION], + discovery_info[ssdp.ATTR_SSDP_UDN], + ) + + if not self._location: + self._location = discovery_info[ssdp.ATTR_SSDP_LOCATION] + assert isinstance(self._location, str) + + self._udn = discovery_info[ssdp.ATTR_SSDP_UDN] + await self.async_set_unique_id(self._udn) + + if abort_if_configured: + # Abort if already configured, but update the last-known location + self._abort_if_unique_id_configured( + updates={CONF_URL: self._location}, reload_on_update=False + ) + + self._device_type = ( + discovery_info.get(ssdp.ATTR_SSDP_NT) or discovery_info[ssdp.ATTR_SSDP_ST] + ) + self._name = ( + discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + or urlparse(self._location).hostname + or DEFAULT_NAME + ) + + async def _async_get_discoveries(self) -> list[Mapping[str, Any]]: """Get list of unconfigured DLNA devices discovered by SSDP.""" LOGGER.debug("_get_discoveries") # Get all compatible devices from ssdp's cache - discoveries: list[Mapping[str, str]] = [] + discoveries: list[Mapping[str, Any]] = [] for udn_st in DmrDevice.DEVICE_TYPES: st_discoveries = await ssdp.async_get_discovery_info_by_st( self.hass, udn_st @@ -235,7 +373,8 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Filter out devices already configured current_unique_ids = { - entry.unique_id for entry in self._async_current_entries() + entry.unique_id + for entry in self._async_current_entries(include_ignore=False) } discoveries = [ disc @@ -245,32 +384,6 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return discoveries - async def _async_connect(self, location: str) -> dict[str, str]: - """Connect to a device to confirm it works and get discovery information. - - Raises ConnectError if something goes wrong. - """ - LOGGER.debug("_async_connect(location=%s)", location) - domain_data = get_domain_data(self.hass) - try: - device = await domain_data.upnp_factory.async_create_device(location) - except UpnpError as err: - raise ConnectError("could_not_connect") from err - - try: - device = find_device_of_type(device, DmrDevice.DEVICE_TYPES) - except UpnpError as err: - raise ConnectError("not_dmr") from err - - discovery = { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_UDN: device.udn, - ssdp.ATTR_SSDP_ST: device.device_type, - ssdp.ATTR_UPNP_FRIENDLY_NAME: device.name, - } - - return discovery - class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): """Handle a DLNA DMR options flow. @@ -315,8 +428,7 @@ class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): def _add_with_suggestion(key: str, validator: Callable) -> None: """Add a field to with a suggested, not default, value.""" - suggested_value = options.get(key) - if suggested_value is None: + if (suggested_value := options.get(key)) is None: fields[vol.Optional(key)] = validator else: fields[ @@ -338,3 +450,41 @@ class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): data_schema=vol.Schema(fields), errors=errors, ) + + +def _is_ignored_device(discovery_info: Mapping[str, Any]) -> bool: + """Return True if this device should be ignored for discovery. + + These devices are supported better by other integrations, so don't bug + the user about them. The user can add them if desired by via the user config + flow, which will list all discovered but unconfigured devices. + """ + # Did the discovery trigger more than just this flow? + if len(discovery_info.get(ssdp.ATTR_HA_MATCHING_DOMAINS, set())) > 1: + LOGGER.debug( + "Ignoring device supported by multiple integrations: %s", + discovery_info[ssdp.ATTR_HA_MATCHING_DOMAINS], + ) + return True + + # Is the root device not a DMR? + if discovery_info.get(ssdp.ATTR_UPNP_DEVICE_TYPE) not in DmrDevice.DEVICE_TYPES: + return True + + # Special cases for devices with other discovery methods (e.g. mDNS), or + # that advertise multiple unrelated (sent in separate discovery packets) + # UPnP devices. + manufacturer = discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER, "").lower() + model = discovery_info.get(ssdp.ATTR_UPNP_MODEL_NAME, "").lower() + + if manufacturer.startswith("xbmc") or model == "kodi": + # kodi + return True + if manufacturer.startswith("samsung") and "tv" in model: + # samsungtv + return True + if manufacturer.startswith("lg") and "tv" in model: + # webostv + return True + + return False diff --git a/homeassistant/components/dlna_dmr/data.py b/homeassistant/components/dlna_dmr/data.py index 8a43fc23763..07046ba4acc 100644 --- a/homeassistant/components/dlna_dmr/data.py +++ b/homeassistant/components/dlna_dmr/data.py @@ -3,8 +3,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Mapping -from typing import Any, NamedTuple, cast +from typing import NamedTuple, cast from async_upnp_client import UpnpEventHandler, UpnpFactory, UpnpRequester from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester @@ -33,7 +32,6 @@ class DlnaDmrData: event_notifiers: dict[EventListenAddr, AiohttpNotifyServer] event_notifier_refs: defaultdict[EventListenAddr, int] stop_listener_remove: CALLBACK_TYPE | None = None - unmigrated_config: dict[str, Mapping[str, Any]] def __init__(self, hass: HomeAssistant) -> None: """Initialize global data.""" @@ -43,11 +41,9 @@ class DlnaDmrData: self.upnp_factory = UpnpFactory(self.requester, non_strict=True) self.event_notifiers = {} self.event_notifier_refs = defaultdict(int) - self.unmigrated_config = {} async def async_cleanup_event_notifiers(self, event: Event) -> None: """Clean up resources when Home Assistant is stopped.""" - del event # unused LOGGER.debug("Cleaning resources in DlnaDmrData") async with self.lock: tasks = (server.stop_server() for server in self.event_notifiers.values()) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 1e8df555a22..962b2e167be 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,8 +3,22 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.22.8"], - "dependencies": ["network", "ssdp"], + "requirements": ["async-upnp-client==0.22.10"], + "dependencies": ["ssdp"], + "ssdp": [ + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", + "st": "urn:schemas-upnp-org:device:MediaRenderer:1" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", + "st": "urn:schemas-upnp-org:device:MediaRenderer:2" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", + "st": "urn:schemas-upnp-org:device:MediaRenderer:3" + } + ], "codeowners": ["@StevenLooman", "@chishm"], "iot_class": "local_push" } diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 809ab71671b..2835117e57c 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -7,8 +7,9 @@ from datetime import datetime, timedelta import functools from typing import Any, Callable, TypeVar, cast -from async_upnp_client import UpnpError, UpnpService, UpnpStateVariable +from async_upnp_client import UpnpService, UpnpStateVariable from async_upnp_client.const import NotificationSubType +from async_upnp_client.exceptions import UpnpError, UpnpResponseError from async_upnp_client.profiles.dlna import DmrDevice, TransportState from async_upnp_client.utils import async_get_local_ip import voluptuous as vol @@ -42,12 +43,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, - DEFAULT_NAME, DOMAIN, LOGGER as _LOGGER, MEDIA_TYPE_MAP, @@ -69,7 +70,7 @@ PLATFORM_SCHEMA = vol.All( vol.Required(CONF_URL): cv.string, vol.Optional(CONF_LISTEN_IP): cv.string, vol.Optional(CONF_LISTEN_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_CALLBACK_URL_OVERRIDE): cv.url, } ), @@ -98,13 +99,35 @@ def catch_request_errors(func: Func) -> Func: return cast(Func, wrapper) +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up DLNA media_player platform.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=config, + ) + ) + + _LOGGER.warning( + "Configuring dlna_dmr via yaml is deprecated; the configuration for" + " %s will be migrated to a config entry and can be safely removed when" + "migration is complete", + config.get(CONF_URL), + ) + + async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the DlnaDmrEntity from a config entry.""" - del hass # Unused _LOGGER.debug("media_player.async_setup_entry %s (%s)", entry.entry_id, entry.title) # Create our own device-wrapping entity @@ -118,10 +141,6 @@ async def async_setup_entry( location=entry.data[CONF_URL], ) - entry.async_on_unload( - entry.add_update_listener(entity.async_config_update_listener) - ) - async_add_entities([entity]) @@ -139,7 +158,6 @@ class DlnaDmrEntity(MediaPlayerEntity): _device_lock: asyncio.Lock # Held when connecting or disconnecting the device _device: DmrDevice | None = None - _remove_ssdp_callbacks: list[Callable] check_available: bool = False # Track BOOTID in SSDP advertisements for device changes @@ -167,10 +185,19 @@ class DlnaDmrEntity(MediaPlayerEntity): self.poll_availability = poll_availability self.location = location self._device_lock = asyncio.Lock() - self._remove_ssdp_callbacks = [] async def async_added_to_hass(self) -> None: """Handle addition.""" + # Update this entity when the associated config entry is modified + if self.registry_entry and self.registry_entry.config_entry_id: + config_entry = self.hass.config_entries.async_get_entry( + self.registry_entry.config_entry_id + ) + assert config_entry is not None + self.async_on_remove( + config_entry.add_update_listener(self.async_config_update_listener) + ) + # Try to connect to the last known location, but don't worry if not available if not self._device: try: @@ -179,7 +206,7 @@ class DlnaDmrEntity(MediaPlayerEntity): _LOGGER.debug("Couldn't connect immediately: %r", err) # Get SSDP notifications for only this device - self._remove_ssdp_callbacks.append( + self.async_on_remove( await ssdp.async_register_callback( self.hass, self.async_ssdp_callback, {"USN": self.usn} ) @@ -189,7 +216,7 @@ class DlnaDmrEntity(MediaPlayerEntity): # (device name) which often is not the USN (service within the device) # that we're interested in. So also listen for byebye advertisements for # the UDN, which is reported in the _udn field of the combined_headers. - self._remove_ssdp_callbacks.append( + self.async_on_remove( await ssdp.async_register_callback( self.hass, self.async_ssdp_callback, @@ -199,9 +226,6 @@ class DlnaDmrEntity(MediaPlayerEntity): async def async_will_remove_from_hass(self) -> None: """Handle removal.""" - for callback in self._remove_ssdp_callbacks: - callback() - self._remove_ssdp_callbacks.clear() await self._device_disconnect() async def async_ssdp_callback( @@ -255,13 +279,12 @@ class DlnaDmrEntity(MediaPlayerEntity): ) # Device could have been de/re-connected, state probably changed - self.schedule_update_ha_state() + self.async_write_ha_state() async def async_config_update_listener( self, hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> None: """Handle options update by modifying self in-place.""" - del hass # Unused _LOGGER.debug( "Updating: %s with data=%s and options=%s", self.name, @@ -292,7 +315,7 @@ class DlnaDmrEntity(MediaPlayerEntity): _LOGGER.warning("Couldn't (re)connect after config change: %r", err) # Device was de/re-connected, state might have changed - self.schedule_update_ha_state() + self.async_write_ha_state() async def _device_connect(self, location: str) -> None: """Connect to the device now that it's available.""" @@ -325,6 +348,10 @@ class DlnaDmrEntity(MediaPlayerEntity): try: self._device.on_event = self._on_event await self._device.async_subscribe_services(auto_resubscribe=True) + except UpnpResponseError as err: + # Device rejected subscription request. This is OK, variables + # will be polled instead. + _LOGGER.debug("Device rejected subscription: %r", err) except UpnpError as err: # Don't leave the device half-constructed self._device.on_event = None @@ -415,11 +442,10 @@ class DlnaDmrEntity(MediaPlayerEntity): self, service: UpnpService, state_variables: Sequence[UpnpStateVariable] ) -> None: """State variable(s) changed, let home-assistant know.""" - del service # Unused if not state_variables: # Indicates a failure to resubscribe, check if device is still available self.check_available = True - self.schedule_update_ha_state() + self.async_write_ha_state() @property def available(self) -> bool: diff --git a/homeassistant/components/dlna_dmr/strings.json b/homeassistant/components/dlna_dmr/strings.json index 27e96b465db..ac77009e0cb 100644 --- a/homeassistant/components/dlna_dmr/strings.json +++ b/homeassistant/components/dlna_dmr/strings.json @@ -3,27 +3,38 @@ "flow_title": "{name}", "step": { "user": { - "title": "DLNA Digital Media Renderer", + "title": "Discovered DLNA DMR devices", + "description": "Choose a device to configure or leave blank to enter a URL", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "manual": { + "title": "Manual DLNA DMR device connection", "description": "URL to a device description XML file", "data": { "url": "[%key:common::config_flow::data::url%]" } }, + "import_turn_on": { + "description": "Please turn on the device and click submit to continue migration" + }, "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "could_not_connect": "Failed to connect to DLNA device", + "alternative_integration": "Device is better supported by another integration", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "discovery_error": "Failed to discover a matching DLNA device", "incomplete_config": "Configuration is missing a required variable", "non_unique_id": "Multiple devices found with the same unique ID", - "not_dmr": "Device is not a Digital Media Renderer" + "not_dmr": "Device is not a supported Digital Media Renderer" }, "error": { - "could_not_connect": "Failed to connect to DLNA device", - "not_dmr": "Device is not a Digital Media Renderer" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "not_dmr": "Device is not a supported Digital Media Renderer" } }, "options": { diff --git a/homeassistant/components/dlna_dmr/translations/bg.json b/homeassistant/components/dlna_dmr/translations/bg.json new file mode 100644 index 00000000000..00e64e1568d --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/bg.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "could_not_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 DLNA \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "incomplete_config": "\u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043b\u0438\u043f\u0441\u0432\u0430 \u0437\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u0430 \u043f\u0440\u043e\u043c\u0435\u043d\u043b\u0438\u0432\u0430", + "non_unique_id": "\u041d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0441\u0430 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0435\u0434\u0438\u043d \u0438 \u0441\u044a\u0449 \u0443\u043d\u0438\u043a\u0430\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "could_not_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 DLNA \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "url": "URL" + }, + "title": "\u041e\u0442\u043a\u0440\u0438\u0442\u0438 DLNA DMR \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } + }, + "options": { + "error": { + "invalid_url": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d URL" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/ca.json b/homeassistant/components/dlna_dmr/translations/ca.json index cf3adc94405..944af3bfebc 100644 --- a/homeassistant/components/dlna_dmr/translations/ca.json +++ b/homeassistant/components/dlna_dmr/translations/ca.json @@ -2,27 +2,41 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", + "alternative_integration": "El dispositiu t\u00e9 millor compatibilitat amb una altra integraci\u00f3", + "cannot_connect": "Ha fallat la connexi\u00f3", "could_not_connect": "No s'ha pogut connectar amb el dispositiu DLNA", "discovery_error": "No s'ha pogut descobrir cap dispositiu DLNA coincident", "incomplete_config": "Falta una variable obligat\u00f2ria a la configuraci\u00f3", "non_unique_id": "S'han trobat diversos dispositius amb el mateix identificador \u00fanic", - "not_dmr": "El dispositiu no \u00e9s un renderitzador de mitjans digitals" + "not_dmr": "El dispositiu no \u00e9s un renderitzador de mitjans digitals compatible" }, "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", "could_not_connect": "No s'ha pogut connectar amb el dispositiu DLNA", - "not_dmr": "El dispositiu no \u00e9s un renderitzador de mitjans digitals" + "not_dmr": "El dispositiu no \u00e9s un renderitzador de mitjans digitals compatible" }, "flow_title": "{name}", "step": { "confirm": { "description": "Vols comen\u00e7ar la configuraci\u00f3?" }, - "user": { + "import_turn_on": { + "description": "Engega el dispositiu i fes clic a Envia per continuar la migraci\u00f3" + }, + "manual": { "data": { "url": "URL" }, - "description": "URL al fitxer XML de descripci\u00f3 de dispositiu", - "title": "Renderitzador de mitjans digitals DLNA" + "description": "URL al fitxer XML de descripci\u00f3 del dispositiu", + "title": "Connexi\u00f3 manual de dispositiu DLNA DMR" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "url": "URL" + }, + "description": "Tria un dispositiu a configurar o deixeu-ho en blanc per introduir un URL", + "title": "Dispositius descoberts DLNA DMR" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/cs.json b/homeassistant/components/dlna_dmr/translations/cs.json new file mode 100644 index 00000000000..85c9a831dda --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Chcete za\u010d\u00edt nastavovat?" + }, + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/de.json b/homeassistant/components/dlna_dmr/translations/de.json index 50f66761748..e37786c401c 100644 --- a/homeassistant/components/dlna_dmr/translations/de.json +++ b/homeassistant/components/dlna_dmr/translations/de.json @@ -2,27 +2,41 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "alternative_integration": "Das Ger\u00e4t wird besser durch eine andere Integration unterst\u00fctzt", + "cannot_connect": "Verbindung fehlgeschlagen", "could_not_connect": "Verbindung zum DLNA-Ger\u00e4t fehlgeschlagen", "discovery_error": "Ein passendes DLNA-Ger\u00e4t konnte nicht gefunden werden", "incomplete_config": "In der Konfiguration fehlt eine erforderliche Variable", "non_unique_id": "Mehrere Ger\u00e4te mit derselben eindeutigen ID gefunden", - "not_dmr": "Ger\u00e4t ist kein Digital Media Renderer" + "not_dmr": "Ger\u00e4t ist kein unterst\u00fctzter Digital Media Renderer" }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", "could_not_connect": "Verbindung zum DLNA-Ger\u00e4t fehlgeschlagen", - "not_dmr": "Ger\u00e4t ist kein Digital Media Renderer" + "not_dmr": "Ger\u00e4t ist kein unterst\u00fctzter Digital Media Renderer" }, "flow_title": "{name}", "step": { "confirm": { "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" }, - "user": { + "import_turn_on": { + "description": "Bitte schalte das Ger\u00e4t ein und klicke auf Senden, um die Migration fortzusetzen" + }, + "manual": { "data": { "url": "URL" }, "description": "URL zu einer XML-Datei mit Ger\u00e4tebeschreibung", - "title": "DLNA Digital Media Renderer" + "title": "Manuelle DLNA DMR-Ger\u00e4teverbindung" + }, + "user": { + "data": { + "host": "Host", + "url": "URL" + }, + "description": "W\u00e4hle ein zu konfigurierendes Ger\u00e4t oder lasse es leer, um eine URL einzugeben.", + "title": "Erkannte DLNA-DMR-Ger\u00e4te" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/en.json b/homeassistant/components/dlna_dmr/translations/en.json index 94bbd365e18..512dfe7f11c 100644 --- a/homeassistant/components/dlna_dmr/translations/en.json +++ b/homeassistant/components/dlna_dmr/translations/en.json @@ -2,27 +2,41 @@ "config": { "abort": { "already_configured": "Device is already configured", + "alternative_integration": "Device is better supported by another integration", + "cannot_connect": "Failed to connect", "could_not_connect": "Failed to connect to DLNA device", "discovery_error": "Failed to discover a matching DLNA device", "incomplete_config": "Configuration is missing a required variable", "non_unique_id": "Multiple devices found with the same unique ID", - "not_dmr": "Device is not a Digital Media Renderer" + "not_dmr": "Device is not a supported Digital Media Renderer" }, "error": { + "cannot_connect": "Failed to connect", "could_not_connect": "Failed to connect to DLNA device", - "not_dmr": "Device is not a Digital Media Renderer" + "not_dmr": "Device is not a supported Digital Media Renderer" }, "flow_title": "{name}", "step": { "confirm": { "description": "Do you want to start set up?" }, - "user": { + "import_turn_on": { + "description": "Please turn on the device and click submit to continue migration" + }, + "manual": { "data": { "url": "URL" }, "description": "URL to a device description XML file", - "title": "DLNA Digital Media Renderer" + "title": "Manual DLNA DMR device connection" + }, + "user": { + "data": { + "host": "Host", + "url": "URL" + }, + "description": "Choose a device to configure or leave blank to enter a URL", + "title": "Discovered DLNA DMR devices" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/et.json b/homeassistant/components/dlna_dmr/translations/et.json index e32101ab251..b5019f5f9a1 100644 --- a/homeassistant/components/dlna_dmr/translations/et.json +++ b/homeassistant/components/dlna_dmr/translations/et.json @@ -2,27 +2,41 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "alternative_integration": "Seadet toetab paremini teine sidumine", + "cannot_connect": "\u00dchendamine nurjus", "could_not_connect": "DLNA seadmega \u00fchenduse loomine nurjus", "discovery_error": "Sobiva DLNA -seadme leidmine nurjus", "incomplete_config": "Seadetes puudub n\u00f5utav muutuja", "non_unique_id": "Leiti mitu sama unikaalse ID-ga seadet", - "not_dmr": "Seade ei ole digitaalse meedia renderdaja" + "not_dmr": "Seade ei ole toetatud digitaalne meediumiedastusseade" }, "error": { + "cannot_connect": "\u00dchendamine nurjus", "could_not_connect": "DLNA seadmega \u00fchenduse loomine nurjus", - "not_dmr": "Seade ei ole digitaalse meedia renderdaja" + "not_dmr": "Seade ei ole toetatud digitaalne meediumiedastusseade" }, "flow_title": "{name}", "step": { "confirm": { "description": "Kas alustada seadistamist?" }, - "user": { + "import_turn_on": { + "description": "L\u00fclita seade sisse ja kl\u00f5psa migreerimise j\u00e4tkamiseks nuppu Edasta" + }, + "manual": { "data": { "url": "URL" }, - "description": "URL aadress seadme kirjelduse XML-failile", - "title": "DLNA digitaalse meediumi renderdaja" + "description": "Seadme kirjelduse XML-faili URL", + "title": "DLNA DMR seadme k\u00e4sitsi \u00fchendamine" + }, + "user": { + "data": { + "host": "Host", + "url": "URL" + }, + "description": "Vali h\u00e4\u00e4lestatav seade v\u00f5i j\u00e4ta URL -i sisestamiseks t\u00fchjaks", + "title": "Avastatud DLNA DMR-seadmed" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/hu.json b/homeassistant/components/dlna_dmr/translations/hu.json index faa7e73eb76..603e14b9e30 100644 --- a/homeassistant/components/dlna_dmr/translations/hu.json +++ b/homeassistant/components/dlna_dmr/translations/hu.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r be van konfigur\u00e1lva", + "alternative_integration": "Az eszk\u00f6zt jobban t\u00e1mogatja egy m\u00e1sik integr\u00e1ci\u00f3", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "could_not_connect": "Nem siker\u00fclt csatlakozni a DLNA-eszk\u00f6zh\u00f6z", "discovery_error": "Nem siker\u00fclt megfelel\u0151 DLNA-eszk\u00f6zt tal\u00e1lni", "incomplete_config": "A konfigur\u00e1ci\u00f3b\u00f3l hi\u00e1nyzik egy sz\u00fcks\u00e9ges \u00e9rt\u00e9k", @@ -9,6 +11,7 @@ "not_dmr": "Az eszk\u00f6z nem digit\u00e1lis m\u00e9dia renderel\u0151" }, "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "could_not_connect": "Nem siker\u00fclt csatlakozni a DLNA-eszk\u00f6zh\u00f6z", "not_dmr": "Az eszk\u00f6z nem digit\u00e1lis m\u00e9dia renderel\u0151" }, @@ -17,8 +20,19 @@ "confirm": { "description": "Kezd\u0151dhet a be\u00e1ll\u00edt\u00e1s?" }, + "import_turn_on": { + "description": "Kapcsolja be az eszk\u00f6zt, \u00e9s kattintson a K\u00fcld\u00e9s gombra a migr\u00e1ci\u00f3 folytat\u00e1s\u00e1hoz" + }, + "manual": { + "data": { + "url": "URL" + }, + "description": "URL egy eszk\u00f6zle\u00edr\u00f3 XML f\u00e1jlhoz", + "title": "DLNA DMR eszk\u00f6z manu\u00e1lis csatlakoztat\u00e1sa" + }, "user": { "data": { + "host": "C\u00edm", "url": "URL" }, "description": "Az eszk\u00f6z le\u00edr\u00e1s\u00e1nak XML-f\u00e1jl URL-c\u00edme", diff --git a/homeassistant/components/dlna_dmr/translations/is.json b/homeassistant/components/dlna_dmr/translations/is.json new file mode 100644 index 00000000000..9ca9e2791b6 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/is.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "import_turn_on": { + "description": "Kveiktu \u00e1 t\u00e6kinu og smelltu \u00e1 senda til a\u00f0 halda \u00e1fram flutningi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/it.json b/homeassistant/components/dlna_dmr/translations/it.json index 5defd82a8be..0ab40e3c804 100644 --- a/homeassistant/components/dlna_dmr/translations/it.json +++ b/homeassistant/components/dlna_dmr/translations/it.json @@ -17,6 +17,9 @@ "confirm": { "description": "Vuoi iniziare la configurazione?" }, + "import_turn_on": { + "description": "Accendi il dispositivo e fai clic su Invia per continuare la migrazione" + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json new file mode 100644 index 00000000000..1a0c1d7fdf4 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + }, + "options": { + "error": { + "invalid_url": "\u7121\u52b9\u306aURL" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/nl.json b/homeassistant/components/dlna_dmr/translations/nl.json index 7387494b9b7..ff14a957526 100644 --- a/homeassistant/components/dlna_dmr/translations/nl.json +++ b/homeassistant/components/dlna_dmr/translations/nl.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", + "alternative_integration": "Apparaat wordt beter ondersteund door een andere integratie", + "cannot_connect": "Kan geen verbinding maken", "could_not_connect": "Mislukt om te verbinden met DNLA apparaat", "discovery_error": "Kan geen overeenkomend DLNA-apparaat vinden", "incomplete_config": "Configuratie mist een vereiste variabele", @@ -9,6 +11,7 @@ "not_dmr": "Apparaat is geen Digital Media Renderer" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "could_not_connect": "Mislukt om te verbinden met DNLA apparaat", "not_dmr": "Apparaat is geen Digital Media Renderer" }, @@ -17,8 +20,19 @@ "confirm": { "description": "Wilt u beginnen met instellen?" }, + "import_turn_on": { + "description": "Zet het apparaat aan en klik op verzenden om door te gaan met de migratie" + }, + "manual": { + "data": { + "url": "URL" + }, + "description": "URL naar een XML-bestand met apparaatbeschrijvingen", + "title": "Handmatige DLNA DMR-apparaatverbinding" + }, "user": { "data": { + "host": "Host", "url": "URL" }, "description": "URL naar een XML-bestand met apparaatbeschrijvingen", diff --git a/homeassistant/components/dlna_dmr/translations/no.json b/homeassistant/components/dlna_dmr/translations/no.json index 1ddbfc32afe..3b0f5854aca 100644 --- a/homeassistant/components/dlna_dmr/translations/no.json +++ b/homeassistant/components/dlna_dmr/translations/no.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", + "alternative_integration": "Enheten st\u00f8ttes bedre av en annen integrasjon", + "cannot_connect": "Tilkobling mislyktes", "could_not_connect": "Kunne ikke koble til DLNA -enhet", "discovery_error": "Kunne ikke finne en matchende DLNA -enhet", "incomplete_config": "Konfigurasjonen mangler en n\u00f8dvendig variabel", @@ -9,6 +11,7 @@ "not_dmr": "Enheten er ikke en Digital Media Renderer" }, "error": { + "cannot_connect": "Tilkobling mislyktes", "could_not_connect": "Kunne ikke koble til DLNA -enhet", "not_dmr": "Enheten er ikke en Digital Media Renderer" }, @@ -17,12 +20,23 @@ "confirm": { "description": "Vil du starte oppsettet?" }, - "user": { + "import_turn_on": { + "description": "Sl\u00e5 p\u00e5 enheten og klikk p\u00e5 send for \u00e5 fortsette overf\u00f8ringen" + }, + "manual": { "data": { "url": "URL" }, "description": "URL til en enhetsbeskrivelse XML -fil", - "title": "DLNA Digital Media Renderer" + "title": "Manuell DLNA DMR -enhetstilkobling" + }, + "user": { + "data": { + "host": "Vert", + "url": "URL" + }, + "description": "Velg en enhet du vil konfigurere, eller la den st\u00e5 tom for \u00e5 angi en URL", + "title": "Oppdaget DLNA DMR -enheter" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/pl.json b/homeassistant/components/dlna_dmr/translations/pl.json new file mode 100644 index 00000000000..09fb3dce509 --- /dev/null +++ b/homeassistant/components/dlna_dmr/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?" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/ru.json b/homeassistant/components/dlna_dmr/translations/ru.json index bf1be8f6c3d..d8931e268a0 100644 --- a/homeassistant/components/dlna_dmr/translations/ru.json +++ b/homeassistant/components/dlna_dmr/translations/ru.json @@ -2,27 +2,41 @@ "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.", + "alternative_integration": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043b\u0443\u0447\u0448\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0440\u0443\u0433\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "could_not_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e DLNA.", "incomplete_config": "\u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f.", "non_unique_id": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0441 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u043c \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u043c \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u043e\u043c.", - "not_dmr": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043c\u0435\u0434\u0438\u0430\u0440\u0435\u043d\u0434\u0435\u0440\u0435\u0440\u043e\u043c (DMR)." + "not_dmr": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 Digital Media Renderer." }, "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "could_not_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", - "not_dmr": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043c\u0435\u0434\u0438\u0430\u0440\u0435\u043d\u0434\u0435\u0440\u0435\u0440\u043e\u043c (DMR)." + "not_dmr": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 Digital Media Renderer." }, "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": { + "import_turn_on": { + "description": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c' \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438" + }, + "manual": { "data": { "url": "URL-\u0430\u0434\u0440\u0435\u0441" }, "description": "URL-\u0430\u0434\u0440\u0435\u0441 XML-\u0444\u0430\u0439\u043b\u0430 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", - "title": "\u041c\u0435\u0434\u0438\u0430\u0440\u0435\u043d\u0434\u0435\u0440\u0435\u0440 DLNA" + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 DLNA DMR" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043b\u0438 \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0432\u0432\u0435\u0441\u0442\u0438 URL.", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 DLNA DMR" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/tr.json b/homeassistant/components/dlna_dmr/translations/tr.json new file mode 100644 index 00000000000..64e3f950b25 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + }, + "options": { + "error": { + "invalid_url": "Ge\u00e7ersiz URL" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hant.json b/homeassistant/components/dlna_dmr/translations/zh-Hant.json index b7eab93d76d..406b23b573f 100644 --- a/homeassistant/components/dlna_dmr/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dmr/translations/zh-Hant.json @@ -2,27 +2,41 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "alternative_integration": "\u4f7f\u7528\u5176\u4ed6\u6574\u5408\u4ee5\u53d6\u5f97\u66f4\u4f73\u7684\u88dd\u7f6e\u652f\u63f4", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "could_not_connect": "DLNA \u88dd\u7f6e\u9023\u7dda\u5931\u6557\u3002", "discovery_error": "DLNA \u88dd\u7f6e\u63a2\u7d22\u5931\u6557", "incomplete_config": "\u6240\u7f3a\u5c11\u7684\u8a2d\u5b9a\u70ba\u5fc5\u9808\u8b8a\u6578", "non_unique_id": "\u627e\u5230\u591a\u7d44\u88dd\u7f6e\u4f7f\u7528\u4e86\u76f8\u540c\u552f\u4e00 ID", - "not_dmr": "\u88dd\u7f6e\u4e26\u975e Digital Media Renderer" + "not_dmr": "\u88dd\u7f6e\u70ba\u975e\u652f\u63f4 Digital Media Renderer" }, "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", "could_not_connect": "DLNA \u88dd\u7f6e\u9023\u7dda\u5931\u6557\u3002", - "not_dmr": "\u88dd\u7f6e\u4e26\u975e Digital Media Renderer" + "not_dmr": "\u88dd\u7f6e\u70ba\u975e\u652f\u63f4 Digital Media Renderer" }, "flow_title": "{name}", "step": { "confirm": { "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" }, - "user": { + "import_turn_on": { + "description": "\u8acb\u958b\u555f\u88dd\u7f6e\u4e26\u9ede\u9078\u50b3\u9001\u4ee5\u7e7c\u7e8c\u9077\u79fb" + }, + "manual": { "data": { "url": "\u7db2\u5740" }, "description": "\u88dd\u7f6e\u8aaa\u660e XML \u6a94\u6848\u4e4b URL", - "title": "DLNA Digital Media Renderer" + "title": "\u624b\u52d5 DLNA DMR \u88dd\u7f6e\u9023\u7dda" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "url": "\u7db2\u5740" + }, + "description": "\u9078\u64c7\u88dd\u7f6e\u9032\u884c\u8a2d\u5b9a\u6216\u4fdd\u7559\u7a7a\u767d\u4ee5\u8f38\u5165 URL", + "title": "\u5df2\u63a2\u7d22\u5230\u7684 DLNA DMR \u88dd\u7f6e" } } }, diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 3e41c1871bf..e4398a756ec 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -154,8 +154,7 @@ class Doods(ImageProcessingEntity): continue # If label confidence is not specified, use global confidence - label_confidence = label.get(CONF_CONFIDENCE) - if not label_confidence: + if not (label_confidence := label.get(CONF_CONFIDENCE)): label_confidence = confidence if label_name not in dconfig or dconfig[label_name] > label_confidence: dconfig[label_name] = label_confidence @@ -187,8 +186,7 @@ class Doods(ImageProcessingEntity): # Handle global detection area self._area = [0, 0, 1, 1] self._covers = True - area_config = config.get(CONF_AREA) - if area_config: + if area_config := config.get(CONF_AREA): self._area = [ area_config[CONF_TOP], area_config[CONF_LEFT], diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index f1addbf477b..3bd82a7ff8e 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -1,4 +1,5 @@ """Support for DoorBird devices.""" +from http import HTTPStatus import logging from aiohttp import web @@ -15,7 +16,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME, - HTTP_UNAUTHORIZED, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -67,9 +67,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def _reset_device_favorites_handler(event): """Handle clearing favorites on device.""" - token = event.data.get("token") - - if token is None: + if (token := event.data.get("token")) is None: return doorstation = get_doorstation_by_token(hass, token) @@ -107,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: status, info = await hass.async_add_executor_job(_init_doorbird_device, device) except requests.exceptions.HTTPError as err: - if err.response.status_code == HTTP_UNAUTHORIZED: + if err.response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error( "Authorization rejected by DoorBird for %s@%s", username, device_ip ) @@ -153,7 +151,7 @@ def _init_doorbird_device(device): return device.ready(), device.info() -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() @@ -332,7 +330,7 @@ class DoorBirdRequestView(HomeAssistantView): if device is None: return web.Response( - status=HTTP_UNAUTHORIZED, text="Invalid token provided." + status=HTTPStatus.UNAUTHORIZED, text="Invalid token provided." ) if device: diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 16b9725cf83..01fcc2b2c22 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -1,4 +1,5 @@ """Config flow for DoorBird integration.""" +from http import HTTPStatus from ipaddress import ip_address import logging @@ -7,13 +8,7 @@ import requests import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_USERNAME, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.util.network import is_link_local @@ -45,7 +40,7 @@ async def validate_input(hass: core.HomeAssistant, data): try: status, info = await hass.async_add_executor_job(_check_device, device) except requests.exceptions.HTTPError as err: - if err.response.status_code == HTTP_UNAUTHORIZED: + if err.response.status_code == HTTPStatus.UNAUTHORIZED: raise InvalidAuth from err raise CannotConnect from err except OSError as err: @@ -66,7 +61,7 @@ async def async_verify_supported_device(hass, host): try: await hass.async_add_executor_job(device.doorbell_state) except requests.exceptions.HTTPError as err: - if err.response.status_code == HTTP_UNAUTHORIZED: + if err.response.status_code == HTTPStatus.UNAUTHORIZED: return True except OSError: return False @@ -104,13 +99,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_doorbird_device") if is_link_local(ip_address(host)): return self.async_abort(reason="link_local_address") - if not await async_verify_supported_device(self.hass, host): - return self.async_abort(reason="not_doorbird_device") await self.async_set_unique_id(macaddress) - self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + self._async_abort_entries_match({CONF_HOST: host}) + + if not await async_verify_supported_device(self.hass, host): + return self.async_abort(reason="not_doorbird_device") + chop_ending = "._axis-video._tcp.local." friendly_hostname = discovery_info["name"] if friendly_hostname.endswith(chop_ending): diff --git a/homeassistant/components/doorbird/entity.py b/homeassistant/components/doorbird/entity.py index 44cbb1f42de..2cf97aa4b57 100644 --- a/homeassistant/components/doorbird/entity.py +++ b/homeassistant/components/doorbird/entity.py @@ -1,7 +1,7 @@ """The DoorBird integration base entity.""" from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( DOORBIRD_INFO_KEY_BUILD_NUMBER, @@ -23,14 +23,15 @@ class DoorBirdEntity(Entity): self._mac_addr = get_mac_address_from_doorstation_info(doorstation_info) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Doorbird device info.""" firmware = self._doorstation_info[DOORBIRD_INFO_KEY_FIRMWARE] firmware_build = self._doorstation_info[DOORBIRD_INFO_KEY_BUILD_NUMBER] - return { - "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_addr)}, - "name": self._doorstation.name, - "manufacturer": MANUFACTURER, - "sw_version": f"{firmware} {firmware_build}", - "model": self._doorstation_info[DOORBIRD_INFO_KEY_DEVICE_TYPE], - } + return DeviceInfo( + configuration_url="https://webadmin.doorbird.com/", + connections={(dr.CONNECTION_NETWORK_MAC, self._mac_addr)}, + manufacturer=MANUFACTURER, + model=self._doorstation_info[DOORBIRD_INFO_KEY_DEVICE_TYPE], + name=self._doorstation.name, + sw_version=f"{firmware} {firmware_build}", + ) diff --git a/homeassistant/components/doorbird/translations/bg.json b/homeassistant/components/doorbird/translations/bg.json new file mode 100644 index 00000000000..628eaf62894 --- /dev/null +++ b/homeassistant/components/doorbird/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dovado/notify.py b/homeassistant/components/dovado/notify.py index 02ce994b1df..c599ad918e8 100644 --- a/homeassistant/components/dovado/notify.py +++ b/homeassistant/components/dovado/notify.py @@ -22,9 +22,7 @@ class DovadoSMSNotificationService(BaseNotificationService): def send_message(self, message, **kwargs): """Send SMS to the specified target phone number.""" - target = kwargs.get(ATTR_TARGET) - - if not target: + if not (target := kwargs.get(ATTR_TARGET)): _LOGGER.error("One target is required") return diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 89aa4a465cf..7cc64a1507b 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -1,4 +1,5 @@ """Support for functionality to download files.""" +from http import HTTPStatus import logging import os import re @@ -7,7 +8,6 @@ import threading import requests import voluptuous as vol -from homeassistant.const import HTTP_OK import homeassistant.helpers.config_validation as cv from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path @@ -78,7 +78,7 @@ def setup(hass, config): req = requests.get(url, stream=True, timeout=10) - if req.status_code != HTTP_OK: + if req.status_code != HTTPStatus.OK: _LOGGER.warning( "Downloading '%s' failed, status_code=%d", url, req.status_code ) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index bd02be7d63e..12b2a17016a 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, EventType, StateType from homeassistant.util import Throttle @@ -230,10 +231,10 @@ class DSMREntity(SensorEntity): if device_serial is None: device_serial = entry.entry_id - self._attr_device_info = { - "identifiers": {(DOMAIN, device_serial)}, - "name": device_name, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_serial)}, + name=device_name, + ) self._attr_unique_id = f"{device_serial}_{entity_description.name}".replace( " ", "_" ) diff --git a/homeassistant/components/dsmr/translations/bg.json b/homeassistant/components/dsmr/translations/bg.json new file mode 100644 index 00000000000..439b8d63d8d --- /dev/null +++ b/homeassistant/components/dsmr/translations/bg.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_communicate": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_communicate": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 DSMR \u0432\u0435\u0440\u0441\u0438\u044f", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "setup_serial": { + "data": { + "dsmr_version": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 DSMR \u0432\u0435\u0440\u0441\u0438\u044f", + "port": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \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": { + "title": "\u041f\u044a\u0442" + }, + "user": { + "data": { + "type": "\u0422\u0438\u043f \u0432\u0440\u044a\u0437\u043a\u0430" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u043d\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/cs.json b/homeassistant/components/dsmr/translations/cs.json index 9b38d280bdf..8078da1b1a2 100644 --- a/homeassistant/components/dsmr/translations/cs.json +++ b/homeassistant/components/dsmr/translations/cs.json @@ -2,6 +2,23 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "step": { + "setup_serial": { + "data": { + "port": "Vyberte za\u0159\u00edzen\u00ed" + }, + "title": "Za\u0159\u00edzen\u00ed" + }, + "setup_serial_manual_path": { + "title": "Cesta" + }, + "user": { + "data": { + "type": "Typ p\u0159ipojen\u00ed" + }, + "title": "Vyberte typ p\u0159ipojen\u00ed" + } } }, "options": { diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index 5b08e8e142c..d443047a171 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring energy usage using the DTE energy bridge.""" +from http import HTTPStatus import logging import requests @@ -9,7 +10,7 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) -from homeassistant.const import CONF_NAME, HTTP_OK +from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -90,7 +91,7 @@ class DteEnergyBridgeSensor(SensorEntity): ) return - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: _LOGGER.warning( "Invalid status_code from DTE Energy Bridge: %s (%s)", response.status_code, diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index b7daf661e63..8fcbb4dcfed 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -6,12 +6,13 @@ https://data.gov.ie/dataset/real-time-passenger-information-rtpi-for-dublin-bus- """ from contextlib import suppress from datetime import datetime, timedelta +from http import HTTPStatus import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, HTTP_OK, TIME_MINUTES +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -144,7 +145,7 @@ class PublicTransportData: response = requests.get(_RESOURCE, params, timeout=10) - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: self.info = [ {ATTR_DUE_AT: "n/a", ATTR_ROUTE: self.route, ATTR_DUE_IN: "n/a"} ] diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index 482b92f768e..437b5b66a99 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -131,11 +131,11 @@ class DuneHDPlayerEntity(MediaPlayerEntity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "identifiers": {(DOMAIN, self._unique_id)}, - "name": DEFAULT_NAME, - "manufacturer": ATTR_MANUFACTURER, - } + return DeviceInfo( + identifiers={(DOMAIN, self._unique_id)}, + manufacturer=ATTR_MANUFACTURER, + name=DEFAULT_NAME, + ) @property def volume_level(self) -> float: diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index 72803f86f02..9bb4f3aeb27 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -64,11 +64,11 @@ class DynaliteBase(Entity): @property def device_info(self) -> DeviceInfo: """Device info for this entity.""" - return { - "identifiers": {(DOMAIN, self._device.unique_id)}, - "name": self.name, - "manufacturer": "Dynalite", - } + return DeviceInfo( + identifiers={(DOMAIN, self._device.unique_id)}, + manufacturer="Dynalite", + name=self.name, + ) async def async_added_to_hass(self) -> None: """Added to hass so need to register to dispatch.""" diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index 4f4c4d7cbba..19993a6498c 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -142,8 +142,7 @@ class DysonClimateEntity(DysonEntity, ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - target_temp = kwargs.get(ATTR_TEMPERATURE) - if target_temp is None: + if (target_temp := kwargs.get(ATTR_TEMPERATURE)) is None: _LOGGER.error("Missing target temperature %s", kwargs) return target_temp = int(target_temp) diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py index bc2158e4db8..e5edf8e0b99 100644 --- a/homeassistant/components/eafm/sensor.py +++ b/homeassistant/components/eafm/sensor.py @@ -8,6 +8,7 @@ import async_timeout from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_METERS from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -121,13 +122,13 @@ class Measurement(CoordinatorEntity, SensorEntity): @property def device_info(self): """Return the device info.""" - return { - "identifiers": {(DOMAIN, "measure-id", self.station_id)}, - "name": self.name, - "manufacturer": "https://environment.data.gov.uk/", - "model": self.parameter_name, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, "measure-id", self.station_id)}, + manufacturer="https://environment.data.gov.uk/", + model=self.parameter_name, + name=self.name, + ) @property def available(self) -> bool: diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index b81a5e6bef6..94c4ade7398 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -1,8 +1,11 @@ """Support for Ecobee binary sensors.""" +from __future__ import annotations + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, BinarySensorEntity, ) +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER @@ -49,7 +52,7 @@ class EcobeeBinarySensor(BinarySensorEntity): return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return device information for this sensor.""" identifier = None model = None @@ -72,12 +75,12 @@ class EcobeeBinarySensor(BinarySensorEntity): break if identifier is not None: - return { - "identifiers": {(DOMAIN, identifier)}, - "name": self.sensor_name, - "manufacturer": MANUFACTURER, - "model": model, - } + return DeviceInfo( + identifiers={(DOMAIN, identifier)}, + manufacturer=MANUFACTURER, + model=model, + name=self.sensor_name, + ) return None @property diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 0e7a5e52fa7..473ba0cdd58 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -32,12 +32,14 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, + PRECISION_HALVES, PRECISION_TENTHS, STATE_ON, TEMP_FAHRENHEIT, ) from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util.temperature import convert from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER @@ -366,20 +368,21 @@ class Thermostat(ClimateEntity): return self.thermostat["identifier"] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information for this ecobee thermostat.""" + model: str | None 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, - } + return DeviceInfo( + identifiers={(DOMAIN, self.thermostat["identifier"])}, + manufacturer=MANUFACTURER, + model=model, + name=self.name, + ) @property def temperature_unit(self): @@ -392,24 +395,29 @@ class Thermostat(ClimateEntity): return PRECISION_TENTHS @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" return self.thermostat["runtime"]["actualTemperature"] / 10.0 @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lower bound temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_HEAT_COOL: - return round(self.thermostat["runtime"]["desiredHeat"] / 10.0) + return self.thermostat["runtime"]["desiredHeat"] / 10.0 return None @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the upper bound temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_HEAT_COOL: - return round(self.thermostat["runtime"]["desiredCool"] / 10.0) + return self.thermostat["runtime"]["desiredCool"] / 10.0 return None + @property + def target_temperature_step(self) -> float: + """Set target temperature step to halves.""" + return PRECISION_HALVES + @property def has_humidifier_control(self): """Return true if humidifier connected to thermostat and set to manual/on mode.""" @@ -436,14 +444,14 @@ class Thermostat(ClimateEntity): return DEFAULT_MAX_HUMIDITY @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_HEAT_COOL: return None if self.hvac_mode == HVAC_MODE_HEAT: - return round(self.thermostat["runtime"]["desiredHeat"] / 10.0) + return self.thermostat["runtime"]["desiredHeat"] / 10.0 if self.hvac_mode == HVAC_MODE_COOL: - return round(self.thermostat["runtime"]["desiredCool"] / 10.0) + return self.thermostat["runtime"]["desiredCool"] / 10.0 return None @property @@ -680,7 +688,7 @@ class Thermostat(ClimateEntity): heat_temp = temp cool_temp = temp else: - delta = self.thermostat["settings"]["heatCoolMinDelta"] / 10 + delta = self.thermostat["settings"]["heatCoolMinDelta"] / 10.0 heat_temp = temp - delta cool_temp = temp + delta self.set_auto_temp_hold(heat_temp, cool_temp) diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index 984609c2f22..b660a75363f 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -1,4 +1,6 @@ """Support for using humidifier with ecobee thermostats.""" +from __future__ import annotations + from datetime import timedelta from homeassistant.components.humidifier import HumidifierEntity @@ -9,6 +11,7 @@ from homeassistant.components.humidifier.const import ( MODE_AUTO, SUPPORT_MODES, ) +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER @@ -54,20 +57,21 @@ class EcobeeHumidifier(HumidifierEntity): return f"{self.thermostat['identifier']}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information for the ecobee humidifier.""" + model: str | None 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, - } + return DeviceInfo( + identifiers={(DOMAIN, self.thermostat["identifier"])}, + manufacturer=MANUFACTURER, + model=model, + name=self.name, + ) @property def available(self): diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 47e7af66e57..cfdf861e7bc 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( PERCENTAGE, TEMP_FAHRENHEIT, ) +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER @@ -76,7 +77,7 @@ class EcobeeSensor(SensorEntity): return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return device information for this sensor.""" identifier = None model = None @@ -99,12 +100,12 @@ class EcobeeSensor(SensorEntity): break if identifier is not None and model is not None: - return { - "identifiers": {(DOMAIN, identifier)}, - "name": self.sensor_name, - "manufacturer": MANUFACTURER, - "model": model, - } + return DeviceInfo( + identifiers={(DOMAIN, identifier)}, + manufacturer=MANUFACTURER, + model=model, + name=self.sensor_name, + ) return None @property diff --git a/homeassistant/components/ecobee/translations/ru.json b/homeassistant/components/ecobee/translations/ru.json index f983a80b389..2bd6ee3ea56 100644 --- a/homeassistant/components/ecobee/translations/ru.json +++ b/homeassistant/components/ecobee/translations/ru.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 https://www.ecobee.com/consumerportal/index.html \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e PIN-\u043a\u043e\u0434\u0430: \n\n {pin} \n \n \u0417\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 https://www.ecobee.com/consumerportal/index.html \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e PIN-\u043a\u043e\u0434\u0430: \n\n {pin} \n \n \u0417\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u043c\u0438\u0442\u0435 ''\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c''.", "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0430 ecobee.com" }, "user": { diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 8e3de2be90a..aa73bd01c53 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -1,4 +1,6 @@ """Support for displaying weather info from Ecobee API.""" +from __future__ import annotations + from datetime import timedelta from pyecobee.const import ECOBEE_STATE_UNKNOWN @@ -13,6 +15,7 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_FAHRENHEIT +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import dt as dt_util from homeassistant.util.pressure import convert as pressure_convert @@ -65,21 +68,22 @@ class EcobeeWeather(WeatherEntity): return self.data.ecobee.get_thermostat(self._index)["identifier"] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information for the ecobee weather platform.""" thermostat = self.data.ecobee.get_thermostat(self._index) + model: str | None try: model = f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" except KeyError: # Ecobee model is not in our list model = None - return { - "identifiers": {(DOMAIN, thermostat["identifier"])}, - "name": self.name, - "manufacturer": MANUFACTURER, - "model": model, - } + return DeviceInfo( + identifiers={(DOMAIN, thermostat["identifier"])}, + manufacturer=MANUFACTURER, + model=model, + name=self.name, + ) @property def condition(self): diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index 5a20337e454..8e39a6a6267 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -16,7 +16,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval from .const import API_CLIENT, DOMAIN, EQUIPMENT @@ -128,13 +128,13 @@ class EcoNetEntity(Entity): return self._econet.connected @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" - return { - "identifiers": {(DOMAIN, self._econet.device_id)}, - "manufacturer": "Rheem", - "name": self._econet.device_name, - } + return DeviceInfo( + identifiers={(DOMAIN, self._econet.device_id)}, + manufacturer="Rheem", + name=self._econet.device_name, + ) @property def name(self): diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py index fe50855d559..24bac516466 100644 --- a/homeassistant/components/econet/climate.py +++ b/homeassistant/components/econet/climate.py @@ -1,6 +1,4 @@ """Support for Rheem EcoNet thermostats.""" -import logging - from pyeconet.equipment import EquipmentType from pyeconet.equipment.thermostat import ThermostatFanMode, ThermostatOperationMode @@ -28,8 +26,6 @@ from homeassistant.const import ATTR_TEMPERATURE from . import EcoNetEntity from .const import DOMAIN, EQUIPMENT -_LOGGER = logging.getLogger(__name__) - ECONET_STATE_TO_HA = { ThermostatOperationMode.HEATING: HVAC_MODE_HEAT, ThermostatOperationMode.COOLING: HVAC_MODE_COOL, diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index ed31e78af7c..7ea4d7740a5 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -113,8 +113,7 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - target_temp = kwargs.get(ATTR_TEMPERATURE) - if target_temp is not None: + if (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None: self.water_heater.set_set_point(target_temp) else: _LOGGER.error("A target temperature must be provided") diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 407f5902198..d9997a9c1e3 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -178,8 +178,7 @@ class EDL21: new_entities = [] for telegram in message_body.get("valList", []): - obis = telegram.get("objName") - if not obis: + if not (obis := telegram.get("objName")): continue if (electricity_id, obis) in self._registered_obis: @@ -187,8 +186,7 @@ class EDL21: self._hass, SIGNAL_EDL21_TELEGRAM, electricity_id, telegram ) else: - name = self._OBIS_NAMES.get(obis) - if name: + if name := self._OBIS_NAMES.get(obis): if self._name: name = f"{self._name}: {name}" new_entities.append( diff --git a/homeassistant/components/efergy/__init__.py b/homeassistant/components/efergy/__init__.py index 8ceeb1585a4..dd6c6001259 100644 --- a/homeassistant/components/efergy/__init__.py +++ b/homeassistant/components/efergy/__init__.py @@ -1 +1,72 @@ -"""The efergy component.""" +"""The Efergy integration.""" +from __future__ import annotations + +from pyefergy import Efergy, exceptions + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import ATTRIBUTION, DATA_KEY_API, DEFAULT_NAME, DOMAIN + +PLATFORMS = [SENSOR_DOMAIN] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Efergy from a config entry.""" + api = Efergy( + entry.data[CONF_API_KEY], + session=async_get_clientsession(hass), + utc_offset=hass.config.time_zone, + currency=hass.config.currency, + ) + + try: + await api.async_status(get_sids=True) + except (exceptions.ConnectError, exceptions.DataError) as ex: + raise ConfigEntryNotReady(f"Failed to connect to device: {ex}") from ex + except exceptions.InvalidAuth as ex: + raise ConfigEntryAuthFailed( + "API Key is no longer valid. Please reauthenticate" + ) from ex + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_KEY_API: api} + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +class EfergyEntity(Entity): + """Representation of a Efergy entity.""" + + def __init__( + self, + api: Efergy, + server_unique_id: str, + ) -> None: + """Initialize an Efergy entity.""" + self.api = api + self._server_unique_id = server_unique_id + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attr_device_info = DeviceInfo( + configuration_url="https://engage.efergy.com/user/login", + connections={(dr.CONNECTION_NETWORK_MAC, self.api.info["mac"])}, + identifiers={(DOMAIN, self._server_unique_id)}, + manufacturer=DEFAULT_NAME, + name=DEFAULT_NAME, + model=self.api.info["type"], + sw_version=self.api.info["version"], + ) diff --git a/homeassistant/components/efergy/config_flow.py b/homeassistant/components/efergy/config_flow.py new file mode 100644 index 00000000000..f386b539768 --- /dev/null +++ b/homeassistant/components/efergy/config_flow.py @@ -0,0 +1,85 @@ +"""Config flow for Efergy integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from pyefergy import Efergy, exceptions +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType + +from .const import CONF_APPTOKEN, DEFAULT_NAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Efergy.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + errors = {} + if user_input is not None: + api_key = user_input[CONF_API_KEY] + + self._async_abort_entries_match({CONF_API_KEY: api_key}) + hid, error = await self._async_try_connect(api_key) + if error is None: + entry = await self.async_set_unique_id(hid) + if entry: + self.hass.config_entries.async_update_entry(entry, data=user_input) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=DEFAULT_NAME, + data={CONF_API_KEY: api_key}, + ) + errors["base"] = error + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } + ), + errors=errors, + ) + + async def async_step_import(self, import_config: ConfigType) -> FlowResult: + """Import a config entry from configuration.yaml.""" + for entry in self._async_current_entries(): + if entry.data[CONF_API_KEY] == import_config[CONF_APPTOKEN]: + _part = import_config[CONF_APPTOKEN][0:4] + _msg = f"Efergy yaml config with partial key {_part} has been imported. Please remove it" + _LOGGER.warning(_msg) + return self.async_abort(reason="already_configured") + return await self.async_step_user({CONF_API_KEY: import_config[CONF_APPTOKEN]}) + + async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + """Handle a reauthorization flow request.""" + return await self.async_step_user() + + async def _async_try_connect(self, api_key: str) -> tuple[str | None, str | None]: + """Try connecting to Efergy servers.""" + api = Efergy(api_key, session=async_get_clientsession(self.hass)) + try: + await api.async_status() + except exceptions.ConnectError: + return None, "cannot_connect" + except exceptions.InvalidAuth: + return None, "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return None, "unknown" + return api.info["hid"], None diff --git a/homeassistant/components/efergy/const.py b/homeassistant/components/efergy/const.py new file mode 100644 index 00000000000..b141c3ebdb8 --- /dev/null +++ b/homeassistant/components/efergy/const.py @@ -0,0 +1,13 @@ +"""Constants for the Efergy integration.""" +from datetime import timedelta + +ATTRIBUTION = "Data provided by Efergy" + +CONF_APPTOKEN = "app_token" +CONF_CURRENT_VALUES = "current_values" + +DATA_KEY_API = "api" +DEFAULT_NAME = "Efergy" +DOMAIN = "efergy" + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index 3b84d243d46..17f104c561f 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -1,8 +1,9 @@ { "domain": "efergy", "name": "Efergy", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/efergy", - "requirements": ["pyefergy==0.0.3"], + "requirements": ["pyefergy==0.1.3"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index a11fe5f3ac6..1f0db12f449 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -2,15 +2,20 @@ from __future__ import annotations import logging +from re import sub from pyefergy import Efergy, exceptions import voluptuous as vol +from homeassistant.components.efergy import EfergyEntity from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_CURRENCY, CONF_MONITORED_VARIABLES, @@ -22,72 +27,113 @@ from homeassistant.const import ( POWER_WATT, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -CONF_APPTOKEN = "app_token" -CONF_UTC_OFFSET = "utc_offset" - -CONF_PERIOD = "period" - -CONF_INSTANT = "instant_readings" -CONF_AMOUNT = "amount" -CONF_BUDGET = "budget" -CONF_COST = "cost" -CONF_CURRENT_VALUES = "current_values" - -DEFAULT_PERIOD = "year" -DEFAULT_UTC_OFFSET = "0" +from .const import CONF_APPTOKEN, CONF_CURRENT_VALUES, DATA_KEY_API, DOMAIN _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES: dict[str, SensorEntityDescription] = { - CONF_INSTANT: SensorEntityDescription( - key=CONF_INSTANT, - name="Energy Usage", +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="instant_readings", + name="Power Usage", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, ), - CONF_AMOUNT: SensorEntityDescription( - key=CONF_AMOUNT, - name="Energy Consumed", + SensorEntityDescription( + key="energy_day", + name="Daily Consumption", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, ), - CONF_BUDGET: SensorEntityDescription( - key=CONF_BUDGET, + SensorEntityDescription( + key="energy_week", + name="Weekly Consumption", + device_class=DEVICE_CLASS_ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="energy_month", + name="Monthly Consumption", + device_class=DEVICE_CLASS_ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="energy_year", + name="Yearly Consumption", + device_class=DEVICE_CLASS_ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="budget", name="Energy Budget", + entity_registry_enabled_default=False, ), - CONF_COST: SensorEntityDescription( - key=CONF_COST, - name="Energy Cost", + SensorEntityDescription( + key="cost_day", + name="Daily Energy Cost", device_class=DEVICE_CLASS_MONETARY, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, ), - CONF_CURRENT_VALUES: SensorEntityDescription( + SensorEntityDescription( + key="cost_week", + name="Weekly Energy Cost", + device_class=DEVICE_CLASS_MONETARY, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="cost_month", + name="Monthly Energy Cost", + device_class=DEVICE_CLASS_MONETARY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="cost_year", + name="Yearly Energy Cost", + device_class=DEVICE_CLASS_MONETARY, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( key=CONF_CURRENT_VALUES, - name="Per-Device Usage", + name="Power Usage", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, ), -} +) + +TYPES_SCHEMA = vol.In( + ["current_values", "instant_readings", "amount", "budget", "cost"] +) -TYPES_SCHEMA = vol.In(SENSOR_TYPES) SENSORS_SCHEMA = vol.Schema( { vol.Required(CONF_TYPE): TYPES_SCHEMA, vol.Optional(CONF_CURRENCY, default=""): cv.string, - vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): cv.string, + vol.Optional("period", default="year"): cv.string, } ) +# Deprecated in Home Assistant 2021.11 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_APPTOKEN): cv.string, - vol.Optional(CONF_UTC_OFFSET, default=DEFAULT_UTC_OFFSET): cv.string, + vol.Optional("utc_offset", default="0"): cv.string, vol.Required(CONF_MONITORED_VARIABLES): [SENSORS_SCHEMA], } ) @@ -97,64 +143,71 @@ async def async_setup_platform( hass: HomeAssistant, config: ConfigType, add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType = None, + discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Efergy sensor.""" - api = Efergy( - config[CONF_APPTOKEN], - async_get_clientsession(hass), - utc_offset=config[CONF_UTC_OFFSET], + """Set up the Efergy sensor from yaml.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) ) - dev = [] - try: - sensors = await api.get_sids() - except (exceptions.DataError, exceptions.ConnectTimeout) as ex: - raise PlatformNotReady("Error getting data from Efergy:") from ex - for variable in config[CONF_MONITORED_VARIABLES]: - if variable[CONF_TYPE] == CONF_CURRENT_VALUES: - for sensor in sensors: - dev.append( + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: entity_platform.AddEntitiesCallback, +) -> None: + """Set up Efergy sensors.""" + api: Efergy = hass.data[DOMAIN][entry.entry_id][DATA_KEY_API] + sensors = [] + for description in SENSOR_TYPES: + if description.key != CONF_CURRENT_VALUES: + sensors.append( + EfergySensor( + api, + description, + entry.entry_id, + period=sub("^energy_|^cost_", "", description.key), + currency=hass.config.currency, + ) + ) + else: + description.entity_registry_enabled_default = len(api.info["sids"]) > 1 + for sid in api.info["sids"]: + sensors.append( EfergySensor( api, - variable[CONF_PERIOD], - variable[CONF_CURRENCY], - SENSOR_TYPES[variable[CONF_TYPE]], - sid=sensor["sid"], + description, + entry.entry_id, + sid=sid, ) ) - dev.append( - EfergySensor( - api, - variable[CONF_PERIOD], - variable[CONF_CURRENCY], - SENSOR_TYPES[variable[CONF_TYPE]], - ) - ) - - add_entities(dev, True) + async_add_entities(sensors, True) -class EfergySensor(SensorEntity): +class EfergySensor(EfergyEntity, SensorEntity): """Implementation of an Efergy sensor.""" def __init__( self, api: Efergy, - period: str, - currency: str, description: SensorEntityDescription, - sid: str = None, + server_unique_id: str, + period: str | None = None, + currency: str | None = None, + sid: str = "", ) -> None: """Initialize the sensor.""" + super().__init__(api, server_unique_id) self.entity_description = description + if description.key == CONF_CURRENT_VALUES: + self._attr_name = f"{description.name}_{sid}" + self._attr_unique_id = f"{server_unique_id}/{description.key}_{sid}" + if "cost" in description.key: + self._attr_native_unit_of_measurement = currency self.sid = sid - self.api = api self.period = period - if sid: - self._attr_name = f"efergy_{sid}" - if description.key == CONF_COST: - self._attr_native_unit_of_measurement = f"{currency}/{period}" async def async_update(self) -> None: """Get the Efergy monitor data from the web service.""" @@ -162,11 +215,11 @@ class EfergySensor(SensorEntity): self._attr_native_value = await self.api.async_get_reading( self.entity_description.key, period=self.period, sid=self.sid ) - except (exceptions.DataError, exceptions.ConnectTimeout) as ex: + except (exceptions.DataError, exceptions.ConnectError) as ex: if self._attr_available: self._attr_available = False - _LOGGER.error("Error getting data from Efergy: %s", ex) + _LOGGER.error("Error getting data: %s", ex) return if not self._attr_available: self._attr_available = True - _LOGGER.info("Connection to Efergy has resumed") + _LOGGER.info("Connection has resumed") diff --git a/homeassistant/components/efergy/strings.json b/homeassistant/components/efergy/strings.json new file mode 100644 index 00000000000..dc625c92840 --- /dev/null +++ b/homeassistant/components/efergy/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "title": "Efergy", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant/components/efergy/translations/bg.json b/homeassistant/components/efergy/translations/bg.json new file mode 100644 index 00000000000..14d4c77c8f9 --- /dev/null +++ b/homeassistant/components/efergy/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/ca.json b/homeassistant/components/efergy/translations/ca.json new file mode 100644 index 00000000000..298826e75e5 --- /dev/null +++ b/homeassistant/components/efergy/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/cs.json b/homeassistant/components/efergy/translations/cs.json new file mode 100644 index 00000000000..b2fe2ea015f --- /dev/null +++ b/homeassistant/components/efergy/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "API kl\u00ed\u010d" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/de.json b/homeassistant/components/efergy/translations/de.json new file mode 100644 index 00000000000..3945d3da7d4 --- /dev/null +++ b/homeassistant/components/efergy/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/en.json b/homeassistant/components/efergy/translations/en.json new file mode 100644 index 00000000000..aa76f9c0636 --- /dev/null +++ b/homeassistant/components/efergy/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/et.json b/homeassistant/components/efergy/translations/et.json new file mode 100644 index 00000000000..73c2f1a3547 --- /dev/null +++ b/homeassistant/components/efergy/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/he.json b/homeassistant/components/efergy/translations/he.json new file mode 100644 index 00000000000..4b0fd849742 --- /dev/null +++ b/homeassistant/components/efergy/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", + "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": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/hu.json b/homeassistant/components/efergy/translations/hu.json new file mode 100644 index 00000000000..032ef05d527 --- /dev/null +++ b/homeassistant/components/efergy/translations/hu.json @@ -0,0 +1,21 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/it.json b/homeassistant/components/efergy/translations/it.json new file mode 100644 index 00000000000..d5677424d42 --- /dev/null +++ b/homeassistant/components/efergy/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/ja.json b/homeassistant/components/efergy/translations/ja.json new file mode 100644 index 00000000000..c2ff6bbb145 --- /dev/null +++ b/homeassistant/components/efergy/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/nl.json b/homeassistant/components/efergy/translations/nl.json new file mode 100644 index 00000000000..4f97bad11a0 --- /dev/null +++ b/homeassistant/components/efergy/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/no.json b/homeassistant/components/efergy/translations/no.json new file mode 100644 index 00000000000..388cbb36f64 --- /dev/null +++ b/homeassistant/components/efergy/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/pl.json b/homeassistant/components/efergy/translations/pl.json new file mode 100644 index 00000000000..9ef0e4a5a43 --- /dev/null +++ b/homeassistant/components/efergy/translations/pl.json @@ -0,0 +1,20 @@ +{ + "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", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/ru.json b/homeassistant/components/efergy/translations/ru.json new file mode 100644 index 00000000000..6a659c9b7c6 --- /dev/null +++ b/homeassistant/components/efergy/translations/ru.json @@ -0,0 +1,21 @@ +{ + "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.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/tr.json b/homeassistant/components/efergy/translations/tr.json new file mode 100644 index 00000000000..212abb7cb64 --- /dev/null +++ b/homeassistant/components/efergy/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/zh-Hant.json b/homeassistant/components/efergy/translations/zh-Hant.json new file mode 100644 index 00000000000..7b51c998fb2 --- /dev/null +++ b/homeassistant/components/efergy/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index b133a96b820..e386ad6beac 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -97,8 +97,7 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): def handle_status_event(self, event): """Handle the Egardia system status event.""" - statuscode = event.get("status") - if statuscode is not None: + if (statuscode := event.get("status")) is not None: status = self.lookupstatusfromcode(statuscode) self.parsestatus(status) self.schedule_update_ha_state() diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index f839b3fcc74..7413e5009de 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -1,8 +1,11 @@ """Support for Eight smart mattress covers and mattresses.""" +from __future__ import annotations + from datetime import timedelta import logging from pyeight.eight import EightSleep +from pyeight.user import EightUser import voluptuous as vol from homeassistant.const import ( @@ -12,23 +15,24 @@ from homeassistant.const import ( CONF_SENSORS, CONF_USERNAME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, ) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) CONF_PARTNER = "partner" DATA_EIGHT = "eight_sleep" +DATA_HEAT = "heat" +DATA_USER = "user" +DATA_API = "api" DOMAIN = "eight_sleep" HEAT_ENTITY = "heat" @@ -100,12 +104,15 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Eight Sleep component.""" - conf = config.get(DOMAIN) - user = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + user = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] if hass.config.time_zone is None: _LOGGER.error("Timezone is not set in Home Assistant") @@ -115,7 +122,7 @@ async def async_setup(hass, config): eight = EightSleep(user, password, timezone, async_get_clientsession(hass)) - hass.data[DATA_EIGHT] = eight + hass.data.setdefault(DATA_EIGHT, {})[DATA_API] = eight # Authenticate, build sensors success = await eight.start() @@ -123,26 +130,14 @@ async def async_setup(hass, config): # Authentication failed, cannot continue return False - async def async_update_heat_data(now): - """Update heat data from eight in HEAT_SCAN_INTERVAL.""" - await eight.update_device_data() - async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT) - - async_track_point_in_utc_time( - hass, async_update_heat_data, utcnow() + HEAT_SCAN_INTERVAL - ) - - async def async_update_user_data(now): - """Update user data from eight in USER_SCAN_INTERVAL.""" - await eight.update_user_data() - async_dispatcher_send(hass, SIGNAL_UPDATE_USER) - - async_track_point_in_utc_time( - hass, async_update_user_data, utcnow() + USER_SCAN_INTERVAL - ) - - await async_update_heat_data(None) - await async_update_user_data(None) + heat_coordinator = hass.data[DOMAIN][DATA_HEAT] = EightSleepHeatDataCoordinator( + hass, eight + ) + user_coordinator = hass.data[DOMAIN][DATA_USER] = EightSleepUserDataCoordinator( + hass, eight + ) + await heat_coordinator.async_config_entry_first_refresh() + await user_coordinator.async_config_entry_first_refresh() # Load sub components sensors = [] @@ -169,7 +164,7 @@ async def async_setup(hass, config): ) ) - async def async_service_handler(service): + async def async_service_handler(service: ServiceCall) -> None: """Handle eight sleep service calls.""" params = service.data.copy() @@ -180,10 +175,10 @@ async def async_setup(hass, config): for sens in sensor: side = sens.split("_")[1] userid = eight.fetch_userid(side) - usrobj = eight.users[userid] + usrobj: EightUser = eight.users[userid] await usrobj.set_heating_level(target, duration) - async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT) + await heat_coordinator.async_request_refresh() # Register services hass.services.async_register( @@ -193,55 +188,40 @@ async def async_setup(hass, config): return True -class EightSleepUserEntity(Entity): - """The Eight Sleep device entity.""" +class EightSleepHeatDataCoordinator(DataUpdateCoordinator): + """Class to retrieve heat data from Eight Sleep.""" - def __init__(self, eight): - """Initialize the data object.""" - self._eight = eight - - async def async_added_to_hass(self): - """Register update dispatcher.""" - - @callback - def async_eight_user_update(): - """Update callback.""" - self.async_schedule_update_ha_state(True) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_USER, async_eight_user_update - ) + def __init__(self, hass: HomeAssistant, api: EightSleep) -> None: + """Initialize coordinator.""" + self.api = api + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN}_heat", + update_interval=HEAT_SCAN_INTERVAL, + update_method=self.api.update_device_data, ) - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False +class EightSleepUserDataCoordinator(DataUpdateCoordinator): + """Class to retrieve user data from Eight Sleep.""" -class EightSleepHeatEntity(Entity): - """The Eight Sleep device entity.""" - - def __init__(self, eight): - """Initialize the data object.""" - self._eight = eight - - async def async_added_to_hass(self): - """Register update dispatcher.""" - - @callback - def async_eight_heat_update(): - """Update callback.""" - self.async_schedule_update_ha_state(True) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_HEAT, async_eight_heat_update - ) + def __init__(self, hass: HomeAssistant, api: EightSleep) -> None: + """Initialize coordinator.""" + self.api = api + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN}_user", + update_interval=USER_SCAN_INTERVAL, + update_method=self.api.update_user_data, ) - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False + +class EightSleepEntity(CoordinatorEntity): + """The Eight Sleep device entity.""" + + def __init__(self, coordinator: DataUpdateCoordinator, eight: EightSleep) -> None: + """Initialize the data object.""" + super().__init__(coordinator) + self._eight = eight diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index d8a763c2e54..5b6e1f6a9c3 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -1,39 +1,65 @@ """Support for Eight Sleep binary sensors.""" import logging +from pyeight.eight import EightSleep +from pyeight.user import EightUser + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, BinarySensorEntity, ) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import CONF_BINARY_SENSORS, DATA_EIGHT, NAME_MAP, EightSleepHeatEntity +from . import ( + CONF_BINARY_SENSORS, + DATA_API, + DATA_EIGHT, + DATA_HEAT, + NAME_MAP, + EightSleepEntity, +) _LOGGER = logging.getLogger(__name__) -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: """Set up the eight sleep binary sensor.""" if discovery_info is None: return name = "Eight" sensors = discovery_info[CONF_BINARY_SENSORS] - eight = hass.data[DATA_EIGHT] + eight = hass.data[DATA_EIGHT][DATA_API] + heat_coordinator = hass.data[DATA_EIGHT][DATA_HEAT] all_sensors = [] for sensor in sensors: - all_sensors.append(EightHeatSensor(name, eight, sensor)) + all_sensors.append(EightHeatSensor(name, heat_coordinator, eight, sensor)) async_add_entities(all_sensors, True) -class EightHeatSensor(EightSleepHeatEntity, BinarySensorEntity): +class EightHeatSensor(EightSleepEntity, BinarySensorEntity): """Representation of a Eight Sleep heat-based sensor.""" - def __init__(self, name, eight, sensor): + def __init__( + self, + name: str, + coordinator: DataUpdateCoordinator, + eight: EightSleep, + sensor: str, + ) -> None: """Initialize the sensor.""" - super().__init__(eight) + super().__init__(coordinator, eight) self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) @@ -41,7 +67,7 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorEntity): self._side = self._sensor.split("_")[0] self._userid = self._eight.fetch_userid(self._side) - self._usrobj = self._eight.users[self._userid] + self._usrobj: EightUser = self._eight.users[self._userid] self._attr_name = f"{name} {self._mapped_name}" self._attr_device_class = DEVICE_CLASS_OCCUPANCY @@ -54,10 +80,12 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorEntity): ) @property - def is_on(self): + def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self._state + return bool(self._state) - async def async_update(self): - """Retrieve latest state.""" + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" self._state = self._usrobj.bed_presence + super()._handle_coordinator_update() diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 1c3944a985e..e722f73c4e7 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -3,6 +3,6 @@ "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", "requirements": ["pyeight==0.1.9"], - "codeowners": ["@mezz64"], + "codeowners": ["@mezz64", "@raman325"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index df0d7882491..c7c58c05e7d 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -1,5 +1,12 @@ """Support for Eight Sleep sensors.""" +from __future__ import annotations + +from collections.abc import Mapping import logging +from typing import Any + +from pyeight.eight import EightSleep +from pyeight.user import EightUser from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( @@ -8,13 +15,20 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import ( CONF_SENSORS, + DATA_API, DATA_EIGHT, + DATA_HEAT, + DATA_USER, NAME_MAP, - EightSleepHeatEntity, - EightSleepUserEntity, + EightSleepEntity, + EightSleepHeatDataCoordinator, + EightSleepUserDataCoordinator, ) ATTR_ROOM_TEMP = "Room Temperature" @@ -45,39 +59,56 @@ ATTR_FIT_WAKEUP_SCORE = "Fitness Wakeup Score" _LOGGER = logging.getLogger(__name__) -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: """Set up the eight sleep sensors.""" if discovery_info is None: return name = "Eight" sensors = discovery_info[CONF_SENSORS] - eight = hass.data[DATA_EIGHT] + eight = hass.data[DATA_EIGHT][DATA_API] + heat_coordinator: EightSleepHeatDataCoordinator = hass.data[DATA_EIGHT][DATA_HEAT] + user_coordinator: EightSleepUserDataCoordinator = hass.data[DATA_EIGHT][DATA_USER] if hass.config.units.is_metric: units = "si" else: units = "us" - all_sensors = [] + all_sensors: list[EightSleepEntity] = [] for sensor in sensors: if "bed_state" in sensor: - all_sensors.append(EightHeatSensor(name, eight, sensor)) + all_sensors.append(EightHeatSensor(name, heat_coordinator, eight, sensor)) elif "room_temp" in sensor: - all_sensors.append(EightRoomSensor(name, eight, sensor, units)) + all_sensors.append( + EightRoomSensor(name, user_coordinator, eight, sensor, units) + ) else: - all_sensors.append(EightUserSensor(name, eight, sensor, units)) + all_sensors.append( + EightUserSensor(name, user_coordinator, eight, sensor, units) + ) async_add_entities(all_sensors, True) -class EightHeatSensor(EightSleepHeatEntity, SensorEntity): +class EightHeatSensor(EightSleepEntity, SensorEntity): """Representation of an eight sleep heat-based sensor.""" - def __init__(self, name, eight, sensor): + def __init__( + self, + name: str, + coordinator: EightSleepHeatDataCoordinator, + eight: EightSleep, + sensor: str, + ) -> None: """Initialize the sensor.""" - super().__init__(eight) + super().__init__(coordinator, eight) self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) @@ -86,7 +117,7 @@ class EightHeatSensor(EightSleepHeatEntity, SensorEntity): self._side = self._sensor.split("_")[0] self._userid = self._eight.fetch_userid(self._side) - self._usrobj = self._eight.users[self._userid] + self._usrobj: EightUser = self._eight.users[self._userid] _LOGGER.debug( "Heat Sensor: %s, Side: %s, User: %s", @@ -96,27 +127,29 @@ class EightHeatSensor(EightSleepHeatEntity, SensorEntity): ) @property - def name(self): + def name(self) -> str: """Return the name of the sensor, if any.""" return self._name @property - def native_value(self): + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._state @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Return the unit the value is expressed in.""" return PERCENTAGE - async def async_update(self): - """Retrieve latest state.""" + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" _LOGGER.debug("Updating Heat sensor: %s", self._sensor) self._state = self._usrobj.heating_level + super()._handle_coordinator_update() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any]: """Return device state attributes.""" return { ATTR_TARGET_HEAT: self._usrobj.target_heating_level, @@ -125,12 +158,19 @@ class EightHeatSensor(EightSleepHeatEntity, SensorEntity): } -class EightUserSensor(EightSleepUserEntity, SensorEntity): +class EightUserSensor(EightSleepEntity, SensorEntity): """Representation of an eight sleep user-based sensor.""" - def __init__(self, name, eight, sensor, units): + def __init__( + self, + name: str, + coordinator: EightSleepUserDataCoordinator, + eight: EightSleep, + sensor: str, + units: str, + ) -> None: """Initialize the sensor.""" - super().__init__(eight) + super().__init__(coordinator, eight) self._sensor = sensor self._sensor_root = self._sensor.split("_", 1)[1] @@ -142,7 +182,7 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): self._side = self._sensor.split("_", 1)[0] self._userid = self._eight.fetch_userid(self._side) - self._usrobj = self._eight.users[self._userid] + self._usrobj: EightUser = self._eight.users[self._userid] _LOGGER.debug( "User Sensor: %s, Side: %s, User: %s", @@ -152,17 +192,17 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): ) @property - def name(self): + def name(self) -> str: """Return the name of the sensor, if any.""" return self._name @property - def native_value(self): + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._state @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if ( "current_sleep" in self._sensor @@ -177,14 +217,15 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): return None @property - def device_class(self): + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" if "bed_temp" in self._sensor: return DEVICE_CLASS_TEMPERATURE return None - async def async_update(self): - """Retrieve latest state.""" + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" _LOGGER.debug("Updating User sensor: %s", self._sensor) if "current" in self._sensor: if "fitness" in self._sensor: @@ -208,8 +249,10 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): elif "sleep_stage" in self._sensor: self._state = self._usrobj.current_values["stage"] + super()._handle_coordinator_update() + @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return device state attributes.""" if self._attr is None: # Skip attributes if sensor type doesn't support @@ -296,12 +339,19 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): return state_attr -class EightRoomSensor(EightSleepUserEntity, SensorEntity): +class EightRoomSensor(EightSleepEntity, SensorEntity): """Representation of an eight sleep room sensor.""" - def __init__(self, name, eight, sensor, units): + def __init__( + self, + name: str, + coordinator: EightSleepUserDataCoordinator, + eight: EightSleep, + sensor: str, + units: str, + ) -> None: """Initialize the sensor.""" - super().__init__(eight) + super().__init__(coordinator, eight) self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) @@ -311,17 +361,18 @@ class EightRoomSensor(EightSleepUserEntity, SensorEntity): self._units = units @property - def name(self): + def name(self) -> str: """Return the name of the sensor, if any.""" return self._name @property - def native_value(self): + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._state - async def async_update(self): - """Retrieve latest state.""" + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" _LOGGER.debug("Updating Room sensor: %s", self._sensor) temp = self._eight.room_temperature() try: @@ -331,15 +382,16 @@ class EightRoomSensor(EightSleepUserEntity, SensorEntity): self._state = round((temp * 1.8) + 32, 2) except TypeError: self._state = None + super()._handle_coordinator_update() @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Return the unit the value is expressed in.""" if self._units == "si": return TEMP_CELSIUS return TEMP_FAHRENHEIT @property - def device_class(self): + def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" return DEVICE_CLASS_TEMPERATURE diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 7f7e432c5f0..8ed443b65e1 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -16,13 +16,6 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, -) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import ( @@ -187,13 +180,13 @@ class ElgatoLight(LightEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this Elgato Light.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._info.serial_number)}, - ATTR_NAME: self._info.product_name, - ATTR_MANUFACTURER: "Elgato", - ATTR_MODEL: self._info.product_name, - ATTR_SW_VERSION: f"{self._info.firmware_version} ({self._info.firmware_build_number})", - } + return DeviceInfo( + identifiers={(DOMAIN, self._info.serial_number)}, + manufacturer="Elgato", + model=self._info.product_name, + name=self._info.product_name, + sw_version=f"{self._info.firmware_version} ({self._info.firmware_build_number})", + ) async def async_identify(self) -> None: """Identify the light, will make it blink.""" diff --git a/homeassistant/components/elgato/translations/bg.json b/homeassistant/components/elgato/translations/bg.json new file mode 100644 index 00000000000..d00cbb30191 --- /dev/null +++ b/homeassistant/components/elgato/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index a392fbd302a..3b59fffe553 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -26,7 +26,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -236,8 +236,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: elk.connect() def _element_changed(element, changeset): - keypress = changeset.get("last_keypress") - if keypress is None: + if (keypress := changeset.get("last_keypress")) is None: return hass.bus.async_fire( @@ -285,7 +284,7 @@ def _find_elk_by_prefix(hass, prefix): return hass.data[DOMAIN][entry_id]["elk"] -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -453,26 +452,26 @@ class ElkEntity(Entity): self._element_callback(self._element, {}) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info connecting via the ElkM1 system.""" - return { - "via_device": (DOMAIN, f"{self._prefix}_system"), - } + return DeviceInfo( + via_device=(DOMAIN, f"{self._prefix}_system"), + ) class ElkAttachedEntity(ElkEntity): """An elk entity that is attached to the elk system.""" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for the underlying ElkM1 system.""" device_name = "ElkM1" if self._prefix: device_name += f" {self._prefix}" - return { - "name": device_name, - "identifiers": {(DOMAIN, f"{self._prefix}_system")}, - "sw_version": self._elk.panel.elkm1_version, - "manufacturer": "ELK Products, Inc.", - "model": "M1", - } + return DeviceInfo( + identifiers={(DOMAIN, f"{self._prefix}_system")}, + manufacturer="ELK Products, Inc.", + model="M1", + name=device_name, + sw_version=self._elk.panel.elkm1_version, + ) diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index c3ed6bbc40d..5b3a20b3448 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -141,8 +141,7 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity): self.async_write_ha_state() def _watch_area(self, area, changeset): - last_log = changeset.get("last_log") - if not last_log: + if not (last_log := changeset.get("last_log")): return # user_number only set for arm/disarm logs if not last_log.get("user_number"): diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 919aad3d012..905aa35ad19 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -25,12 +25,17 @@ from .const import CONF_AUTO_CONFIGURE, DOMAIN _LOGGER = logging.getLogger(__name__) -PROTOCOL_MAP = {"secure": "elks://", "non-secure": "elk://", "serial": "serial://"} +PROTOCOL_MAP = { + "secure": "elks://", + "TLS 1.2": "elksv1_2://", + "non-secure": "elk://", + "serial": "serial://", +} DATA_SCHEMA = vol.Schema( { vol.Required(CONF_PROTOCOL, default="secure"): vol.In( - ["secure", "non-secure", "serial"] + ["secure", "TLS 1.2", "non-secure", "serial"] ), vol.Required(CONF_ADDRESS): str, vol.Optional(CONF_USERNAME, default=""): str, @@ -55,7 +60,7 @@ async def validate_input(data): prefix = data[CONF_PREFIX] url = _make_url_from_data(data) - requires_password = url.startswith("elks://") + requires_password = url.startswith("elks://") or url.startswith("elksv1_2") if requires_password and (not userid or not password): raise InvalidAuth @@ -74,8 +79,7 @@ async def validate_input(data): def _make_url_from_data(data): - host = data.get(CONF_HOST) - if host: + if host := data.get(CONF_HOST): return host protocol = PROTOCOL_MAP[data[CONF_PROTOCOL]] diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 30fe87103c7..b5bab021bca 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -9,7 +9,7 @@ from elkm1_lib.util import pretty_const, username import voluptuous as vol from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ELECTRIC_POTENTIAL_VOLT +from homeassistant.const import ELECTRIC_POTENTIAL_VOLT, ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform @@ -158,6 +158,8 @@ class ElkKeypad(ElkSensor): class ElkPanel(ElkSensor): """Representation of an Elk-M1 Panel.""" + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + @property def icon(self): """Icon to use in the frontend.""" diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 033f7878b5e..68a28cf2846 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -1,5 +1,6 @@ """Support for monitoring emoncms feeds.""" from datetime import timedelta +from http import HTTPStatus import logging import requests @@ -20,7 +21,6 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, - HTTP_OK, POWER_WATT, STATE_UNKNOWN, ) @@ -109,8 +109,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if sensor_names is not None: name = sensor_names.get(int(elem["id"]), None) - unit = elem.get("unit") - if unit: + if unit := elem.get("unit"): unit_of_measurement = unit else: unit_of_measurement = config_unit @@ -257,7 +256,7 @@ class EmonCmsData: _LOGGER.error(exception) return else: - if req.status_code == HTTP_OK: + if req.status_code == HTTPStatus.OK: self.data = req.json() else: _LOGGER.error( diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py index 85b48c55755..5cb639de67c 100644 --- a/homeassistant/components/emoncms_history/__init__.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -1,5 +1,6 @@ """Support for sending data to Emoncms.""" from datetime import timedelta +from http import HTTPStatus import logging import requests @@ -10,7 +11,6 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_URL, CONF_WHITELIST, - HTTP_OK, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -59,7 +59,7 @@ def setup(hass, config): _LOGGER.error("Error saving data '%s' to '%s'", payload, fullurl) else: - if req.status_code != HTTP_OK: + if req.status_code != HTTPStatus.OK: _LOGGER.error( "Error saving data %s to %s (http status code = %d)", payload, diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py index 91263db5127..1cff3c9c30e 100644 --- a/homeassistant/components/emonitor/__init__.py +++ b/homeassistant/components/emonitor/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index 1d699b42473..5d4f1983ac1 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -94,9 +94,9 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): @property def device_info(self) -> DeviceInfo: """Return info about the emonitor device.""" - return { - "name": name_short_mac(self.mac_address[-6:]), - "connections": {(dr.CONNECTION_NETWORK_MAC, self.mac_address)}, - "manufacturer": "Powerhouse Dynamics, Inc.", - "sw_version": self.coordinator.data.hardware.firmware_version, - } + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)}, + manufacturer="Powerhouse Dynamics, Inc.", + name=name_short_mac(self.mac_address[-6:]), + sw_version=self.coordinator.data.hardware.firmware_version, + ) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index a7106f5105f..3b5d1e7831e 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -294,9 +294,7 @@ class HueOneLightStateView(HomeAssistantView): ) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) - entity = hass.states.get(hass_entity_id) - - if entity is None: + if (entity := hass.states.get(hass_entity_id)) is None: _LOGGER.error("Entity not found: %s", hass_entity_id) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) @@ -333,9 +331,7 @@ class HueOneLightChangeView(HomeAssistantView): _LOGGER.error("Unknown entity number: %s", entity_number) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) - entity = hass.states.get(entity_id) - - if entity is None: + if (entity := hass.states.get(entity_id)) is None: _LOGGER.error("Entity not found: %s", entity_id) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) @@ -544,8 +540,7 @@ class HueOneLightChangeView(HomeAssistantView): ): domain = entity.domain # Convert 0-100 to a fan speed - brightness = parsed[STATE_BRIGHTNESS] - if brightness == 0: + if (brightness := parsed[STATE_BRIGHTNESS]) == 0: data[ATTR_SPEED] = SPEED_OFF elif 0 < brightness <= 33.3: data[ATTR_SPEED] = SPEED_LOW diff --git a/homeassistant/components/emulated_kasa/__init__.py b/homeassistant/components/emulated_kasa/__init__.py index d513669cd00..967edc8d157 100644 --- a/homeassistant/components/emulated_kasa/__init__.py +++ b/homeassistant/components/emulated_kasa/__init__.py @@ -51,8 +51,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the emulated_kasa component.""" - conf = config.get(DOMAIN) - if not conf: + if not (conf := config.get(DOMAIN)): return True entity_configs = conf[CONF_ENTITIES] @@ -83,13 +82,11 @@ async def validate_configs(hass, entity_configs): """Validate that entities exist and ensure templates are ready to use.""" entity_registry = await hass.helpers.entity_registry.async_get_registry() for entity_id, entity_config in entity_configs.items(): - state = hass.states.get(entity_id) - if state is None: + if (state := hass.states.get(entity_id)) is None: _LOGGER.debug("Entity not found: %s", entity_id) continue - entity = entity_registry.async_get(entity_id) - if entity: + if entity := entity_registry.async_get(entity_id): entity_config[CONF_UNIQUE_ID] = get_system_unique_id(entity) else: entity_config[CONF_UNIQUE_ID] = entity_id @@ -122,8 +119,7 @@ def get_system_unique_id(entity: RegistryEntry): def get_plug_devices(hass, entity_configs): """Produce list of plug devices from config entities.""" for entity_id, entity_config in entity_configs.items(): - state = hass.states.get(entity_id) - if state is None: + if (state := hass.states.get(entity_id)) is None: continue name = entity_config.get(CONF_NAME, state.name) diff --git a/homeassistant/components/emulated_roku/__init__.py b/homeassistant/components/emulated_roku/__init__.py index 45c9355603f..32e08342191 100644 --- a/homeassistant/components/emulated_roku/__init__.py +++ b/homeassistant/components/emulated_roku/__init__.py @@ -46,9 +46,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the emulated roku component.""" - conf = config.get(DOMAIN) - - if conf is None: + if (conf := config.get(DOMAIN)) is None: return True existing_servers = configured_servers(hass) diff --git a/homeassistant/components/emulated_roku/translations/bg.json b/homeassistant/components/emulated_roku/translations/bg.json index a1a0fd75c60..9bdff95e9b3 100644 --- a/homeassistant/components/emulated_roku/translations/bg.json +++ b/homeassistant/components/emulated_roku/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/emulated_roku/translations/cs.json b/homeassistant/components/emulated_roku/translations/cs.json index c84810814ed..0c44bb73ff0 100644 --- a/homeassistant/components/emulated_roku/translations/cs.json +++ b/homeassistant/components/emulated_roku/translations/cs.json @@ -6,9 +6,12 @@ "step": { "user": { "data": { + "advertise_port": "Port odesl\u00e1n\u00ed", "host_ip": "IP adresa hostitele", + "listen_port": "Port p\u0159\u00edjmu", "name": "Jm\u00e9no" - } + }, + "title": "Definice konfigurace serveru" } } }, diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 0c4c5eeb3b9..8cd5702deb7 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -20,10 +20,17 @@ from homeassistant.components.sensor.recorder import reset_detected from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, VOLUME_CUBIC_METERS, ) -from homeassistant.core import HomeAssistant, State, callback, split_entity_id +from homeassistant.core import ( + HomeAssistant, + State, + callback, + split_entity_id, + valid_entity_id, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -37,6 +44,8 @@ SUPPORTED_STATE_CLASSES = [ STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, ] +VALID_ENERGY_UNITS = [ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR] +VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS _LOGGER = logging.getLogger(__name__) @@ -177,9 +186,13 @@ class SensorManager: # Make sure the right data is there # If the entity existed, we don't pop it from to_remove so it's removed - if config.get(adapter.entity_energy_key) is None or ( - config.get("entity_energy_price") is None - and config.get("number_energy_price") is None + if ( + config.get(adapter.entity_energy_key) is None + or not valid_entity_id(config[adapter.entity_energy_key]) + or ( + config.get("entity_energy_price") is None + and config.get("number_energy_price") is None + ) ): return @@ -279,14 +292,16 @@ class EnergyCostSensor(SensorEntity): except ValueError: return - if ( - self._adapter.source_type == "grid" - and energy_price_state.attributes.get( - ATTR_UNIT_OF_MEASUREMENT, "" - ).endswith(f"/{ENERGY_WATT_HOUR}") + if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( + f"/{ENERGY_WATT_HOUR}" ): energy_price *= 1000.0 + if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( + f"/{ENERGY_MEGA_WATT_HOUR}" + ): + energy_price /= 1000.0 + else: energy_price_state = None energy_price = cast(float, self._config["number_energy_price"]) @@ -299,15 +314,18 @@ class EnergyCostSensor(SensorEntity): energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if self._adapter.source_type == "grid": - if energy_unit == ENERGY_WATT_HOUR: - energy_price /= 1000 - elif energy_unit != ENERGY_KILO_WATT_HOUR: + if energy_unit not in VALID_ENERGY_UNITS: energy_unit = None elif self._adapter.source_type == "gas": - if energy_unit != VOLUME_CUBIC_METERS: + if energy_unit not in VALID_ENERGY_UNITS_GAS: energy_unit = None + if energy_unit == ENERGY_WATT_HOUR: + energy_price /= 1000 + elif energy_unit == ENERGY_MEGA_WATT_HOUR: + energy_price *= 1000 + if energy_unit is None: if not self._wrong_unit_reported: self._wrong_unit_reported = True @@ -330,6 +348,7 @@ class EnergyCostSensor(SensorEntity): cast(str, self._config[self._adapter.entity_energy_key]), energy, float(self._last_energy_sensor_state.state), + self._last_energy_sensor_state, ): # Energy meter was reset, reset cost sensor too energy_state_copy = copy.copy(energy_state) diff --git a/homeassistant/components/energy/translations/bg.json b/homeassistant/components/energy/translations/bg.json new file mode 100644 index 00000000000..cada66c2ac2 --- /dev/null +++ b/homeassistant/components/energy/translations/bg.json @@ -0,0 +1,3 @@ +{ + "title": "\u0415\u043d\u0435\u0440\u0433\u0438\u044f" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/da.json b/homeassistant/components/energy/translations/da.json new file mode 100644 index 00000000000..168ae4ae877 --- /dev/null +++ b/homeassistant/components/energy/translations/da.json @@ -0,0 +1,3 @@ +{ + "title": "Energi" +} \ No newline at end of file diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 24d060b4352..b2a939bffce 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping, Sequence import dataclasses +import functools from typing import Any from homeassistant.components import recorder, sensor @@ -66,56 +67,66 @@ class EnergyPreferencesValidation: return dataclasses.asdict(self) -@callback -def _async_validate_usage_stat( +async def _async_validate_usage_stat( hass: HomeAssistant, - stat_value: str, + stat_id: str, allowed_device_classes: Sequence[str], allowed_units: Mapping[str, Sequence[str]], unit_error: str, result: list[ValidationIssue], ) -> None: """Validate a statistic.""" - has_entity_source = valid_entity_id(stat_value) + metadata = await hass.async_add_executor_job( + functools.partial( + recorder.statistics.get_metadata, + hass, + statistic_ids=(stat_id,), + ) + ) + + if stat_id not in metadata: + result.append(ValidationIssue("statistics_not_defined", stat_id)) + + has_entity_source = valid_entity_id(stat_id) if not has_entity_source: return - if not recorder.is_entity_recorded(hass, stat_value): + entity_id = stat_id + + if not recorder.is_entity_recorded(hass, entity_id): result.append( ValidationIssue( "recorder_untracked", - stat_value, + entity_id, ) ) return - state = hass.states.get(stat_value) - - if state is None: + if (state := hass.states.get(entity_id)) is None: result.append( ValidationIssue( "entity_not_defined", - stat_value, + entity_id, ) ) return if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): - result.append(ValidationIssue("entity_unavailable", stat_value, state.state)) + result.append(ValidationIssue("entity_unavailable", entity_id, state.state)) return try: current_value: float | None = float(state.state) except ValueError: result.append( - ValidationIssue("entity_state_non_numeric", stat_value, state.state) + ValidationIssue("entity_state_non_numeric", entity_id, state.state) ) return if current_value is not None and current_value < 0: result.append( - ValidationIssue("entity_negative_state", stat_value, current_value) + ValidationIssue("entity_negative_state", entity_id, current_value) ) device_class = state.attributes.get(ATTR_DEVICE_CLASS) @@ -123,7 +134,7 @@ def _async_validate_usage_stat( result.append( ValidationIssue( "entity_unexpected_device_class", - stat_value, + entity_id, device_class, ) ) @@ -131,7 +142,7 @@ def _async_validate_usage_stat( unit = state.attributes.get("unit_of_measurement") if device_class and unit not in allowed_units.get(device_class, []): - result.append(ValidationIssue(unit_error, stat_value, unit)) + result.append(ValidationIssue(unit_error, entity_id, unit)) state_class = state.attributes.get(sensor.ATTR_STATE_CLASS) @@ -144,7 +155,7 @@ def _async_validate_usage_stat( result.append( ValidationIssue( "entity_unexpected_state_class", - stat_value, + entity_id, state_class, ) ) @@ -154,7 +165,7 @@ def _async_validate_usage_stat( and sensor.ATTR_LAST_RESET not in state.attributes ): result.append( - ValidationIssue("entity_state_class_measurement_no_last_reset", stat_value) + ValidationIssue("entity_state_class_measurement_no_last_reset", entity_id) ) @@ -167,9 +178,7 @@ def _async_validate_price_entity( unit_error: str, ) -> None: """Validate that the price entity is correct.""" - state = hass.states.get(entity_id) - - if state is None: + if (state := hass.states.get(entity_id)) is None: result.append( ValidationIssue( "entity_not_defined", @@ -192,33 +201,31 @@ def _async_validate_price_entity( result.append(ValidationIssue(unit_error, entity_id, unit)) -@callback -def _async_validate_cost_stat( +async def _async_validate_cost_stat( hass: HomeAssistant, stat_id: str, result: list[ValidationIssue] ) -> None: """Validate that the cost stat is correct.""" + metadata = await hass.async_add_executor_job( + functools.partial( + recorder.statistics.get_metadata, + hass, + statistic_ids=(stat_id,), + ) + ) + + if stat_id not in metadata: + result.append(ValidationIssue("statistics_not_defined", stat_id)) + has_entity = valid_entity_id(stat_id) if not has_entity: return if not recorder.is_entity_recorded(hass, stat_id): - result.append( - ValidationIssue( - "recorder_untracked", - stat_id, - ) - ) + result.append(ValidationIssue("recorder_untracked", stat_id)) - state = hass.states.get(stat_id) - - if state is None: - result.append( - ValidationIssue( - "entity_not_defined", - stat_id, - ) - ) + if (state := hass.states.get(stat_id)) is None: + result.append(ValidationIssue("entity_not_defined", stat_id)) return state_class = state.attributes.get("state_class") @@ -244,16 +251,16 @@ def _async_validate_cost_stat( @callback def _async_validate_auto_generated_cost_entity( - hass: HomeAssistant, entity_id: str, result: list[ValidationIssue] + hass: HomeAssistant, energy_entity_id: str, result: list[ValidationIssue] ) -> None: """Validate that the auto generated cost entity is correct.""" - if not recorder.is_entity_recorded(hass, entity_id): - result.append( - ValidationIssue( - "recorder_untracked", - entity_id, - ) - ) + if energy_entity_id not in hass.data[DOMAIN]["cost_sensors"]: + # The cost entity has not been setup + return + + cost_entity_id = hass.data[DOMAIN]["cost_sensors"][energy_entity_id] + if not recorder.is_entity_recorded(hass, cost_entity_id): + result.append(ValidationIssue("recorder_untracked", cost_entity_id)) async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: @@ -271,7 +278,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: if source["type"] == "grid": for flow in source["flow_from"]: - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, flow["stat_energy_from"], ENERGY_USAGE_DEVICE_CLASSES, @@ -281,7 +288,9 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) if flow.get("stat_cost") is not None: - _async_validate_cost_stat(hass, flow["stat_cost"], source_result) + await _async_validate_cost_stat( + hass, flow["stat_cost"], source_result + ) elif flow.get("entity_energy_price") is not None: _async_validate_price_entity( hass, @@ -291,18 +300,18 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ENERGY_PRICE_UNIT_ERROR, ) - if ( + if flow.get("entity_energy_from") is not None and ( flow.get("entity_energy_price") is not None or flow.get("number_energy_price") is not None ): _async_validate_auto_generated_cost_entity( hass, - hass.data[DOMAIN]["cost_sensors"][flow["stat_energy_from"]], + flow["entity_energy_from"], source_result, ) for flow in source["flow_to"]: - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, flow["stat_energy_to"], ENERGY_USAGE_DEVICE_CLASSES, @@ -312,7 +321,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) if flow.get("stat_compensation") is not None: - _async_validate_cost_stat( + await _async_validate_cost_stat( hass, flow["stat_compensation"], source_result ) elif flow.get("entity_energy_price") is not None: @@ -324,18 +333,18 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ENERGY_PRICE_UNIT_ERROR, ) - if ( + if flow.get("entity_energy_to") is not None and ( flow.get("entity_energy_price") is not None or flow.get("number_energy_price") is not None ): _async_validate_auto_generated_cost_entity( hass, - hass.data[DOMAIN]["cost_sensors"][flow["stat_energy_to"]], + flow["entity_energy_to"], source_result, ) elif source["type"] == "gas": - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, source["stat_energy_from"], GAS_USAGE_DEVICE_CLASSES, @@ -345,7 +354,9 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) if source.get("stat_cost") is not None: - _async_validate_cost_stat(hass, source["stat_cost"], source_result) + await _async_validate_cost_stat( + hass, source["stat_cost"], source_result + ) elif source.get("entity_energy_price") is not None: _async_validate_price_entity( hass, @@ -355,18 +366,18 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: GAS_PRICE_UNIT_ERROR, ) - if ( + if source.get("entity_energy_from") is not None and ( source.get("entity_energy_price") is not None or source.get("number_energy_price") is not None ): _async_validate_auto_generated_cost_entity( hass, - hass.data[DOMAIN]["cost_sensors"][source["stat_energy_from"]], + source["entity_energy_from"], source_result, ) elif source["type"] == "solar": - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, source["stat_energy_from"], ENERGY_USAGE_DEVICE_CLASSES, @@ -376,7 +387,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) elif source["type"] == "battery": - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, source["stat_energy_from"], ENERGY_USAGE_DEVICE_CLASSES, @@ -384,7 +395,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ENERGY_UNIT_ERROR, source_result, ) - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, source["stat_energy_to"], ENERGY_USAGE_DEVICE_CLASSES, @@ -396,7 +407,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: for device in manager.data["device_consumption"]: device_result: list[ValidationIssue] = [] result.device_consumption.append(device_result) - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, device["stat_consumption"], ENERGY_USAGE_DEVICE_CLASSES, diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index b8ea753b8d0..d743b6c3346 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -73,8 +73,7 @@ class EnOceanLight(EnOceanEntity, LightEntity): def turn_on(self, **kwargs): """Turn the light source on or sets a specific dimmer value.""" - brightness = kwargs.get(ATTR_BRIGHTNESS) - if brightness is not None: + if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: self._brightness = brightness bval = math.floor(self._brightness / 256.0 * 100.0) diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index ccf01eec448..ca0f5e95109 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, ) @@ -44,6 +45,7 @@ SENSOR_DESC_TEMPERATURE = SensorEntityDescription( native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:thermometer", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) SENSOR_DESC_HUMIDITY = SensorEntityDescription( @@ -52,6 +54,7 @@ SENSOR_DESC_HUMIDITY = SensorEntityDescription( native_unit_of_measurement=PERCENTAGE, icon="mdi:water-percent", device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ) SENSOR_DESC_POWER = SensorEntityDescription( @@ -60,6 +63,7 @@ SENSOR_DESC_POWER = SensorEntityDescription( native_unit_of_measurement=POWER_WATT, icon="mdi:power-plug", device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ) SENSOR_DESC_WINDOWHANDLE = SensorEntityDescription( diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 9bf4073847e..eda3c229255 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import COORDINATOR, DOMAIN, NAME, SENSORS @@ -169,13 +170,13 @@ class Envoy(CoordinatorEntity, SensorEntity): return None @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" if not self._device_serial_number: return None - return { - "identifiers": {(DOMAIN, str(self._device_serial_number))}, - "name": self._device_name, - "model": "Envoy", - "manufacturer": "Enphase", - } + return DeviceInfo( + identifiers={(DOMAIN, str(self._device_serial_number))}, + manufacturer="Enphase", + model="Envoy", + name=self._device_name, + ) diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index 3256b26171b..776d1c17618 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -206,8 +206,7 @@ class EnturPublicTransportSensor(SensorEntity): self._attributes[CONF_LATITUDE] = data.latitude self._attributes[CONF_LONGITUDE] = data.longitude - calls = data.estimated_calls - if not calls: + if not (calls := data.estimated_calls): self._state = None return diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index 356e18fe23f..01d74179f41 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -1 +1,108 @@ -"""A component for Environment Canada weather.""" +"""The Environment Canada (EC) component.""" +from datetime import timedelta +import logging +import xml.etree.ElementTree as et + +from env_canada import ECRadar, ECWeather, ec_exc + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_LANGUAGE, CONF_STATION, DOMAIN + +DEFAULT_RADAR_UPDATE_INTERVAL = timedelta(minutes=5) +DEFAULT_WEATHER_UPDATE_INTERVAL = timedelta(minutes=5) + +PLATFORMS = ["camera", "sensor", "weather"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry): + """Set up EC as config entry.""" + lat = config_entry.data.get(CONF_LATITUDE) + lon = config_entry.data.get(CONF_LONGITUDE) + station = config_entry.data.get(CONF_STATION) + lang = config_entry.data.get(CONF_LANGUAGE, "English") + + coordinators = {} + + weather_data = ECWeather( + station_id=station, + coordinates=(lat, lon), + language=lang.lower(), + ) + coordinators["weather_coordinator"] = ECDataUpdateCoordinator( + hass, weather_data, "weather", DEFAULT_WEATHER_UPDATE_INTERVAL + ) + await coordinators["weather_coordinator"].async_config_entry_first_refresh() + + radar_data = ECRadar(coordinates=(lat, lon)) + coordinators["radar_coordinator"] = ECDataUpdateCoordinator( + hass, radar_data, "radar", DEFAULT_RADAR_UPDATE_INTERVAL + ) + await coordinators["radar_coordinator"].async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = coordinators + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +def trigger_import(hass, config): + """Trigger a import of YAML config into a config_entry.""" + _LOGGER.warning( + "Environment Canada YAML configuration is deprecated; your YAML configuration " + "has been imported into the UI and can be safely removed" + ) + if not config.get(CONF_LANGUAGE): + config[CONF_LANGUAGE] = "English" + + data = {} + for key in ( + CONF_STATION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_LANGUAGE, + ): # pylint: disable=consider-using-tuple + if config.get(key): + data[key] = config[key] + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=data + ) + ) + + +class ECDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching EC data.""" + + def __init__(self, hass, ec_data, name, update_interval): + """Initialize global EC data updater.""" + super().__init__( + hass, _LOGGER, name=f"{DOMAIN} {name}", update_interval=update_interval + ) + self.ec_data = ec_data + + async def _async_update_data(self): + """Fetch data from EC.""" + try: + await self.ec_data.update() + except (et.ParseError, ec_exc.UnknownStationId) as ex: + raise UpdateFailed(f"Error fetching {self.name} data: {ex}") from ex + return self.ec_data diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index ecd0c562d16..a4707bb7576 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -1,30 +1,19 @@ """Support for the Environment Canada radar imagery.""" from __future__ import annotations -import datetime - -from env_canada import ECRadar import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, -) +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import CoordinatorEntity -ATTR_UPDATED = "updated" +from . import trigger_import +from .const import ATTR_OBSERVATION_TIME, CONF_STATION, DOMAIN -CONF_ATTRIBUTION = "Data provided by Environment Canada" -CONF_STATION = "station" CONF_LOOP = "loop" CONF_PRECIP_TYPE = "precip_type" -MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=10) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LOOP, default=True): cv.boolean, @@ -37,63 +26,48 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Environment Canada camera.""" + lat = config.get(CONF_LATITUDE, hass.config.latitude) + lon = config.get(CONF_LONGITUDE, hass.config.longitude) - if config.get(CONF_STATION): - radar_object = ECRadar( - station_id=config[CONF_STATION], precip_type=config.get(CONF_PRECIP_TYPE) - ) - else: - lat = config.get(CONF_LATITUDE, hass.config.latitude) - lon = config.get(CONF_LONGITUDE, hass.config.longitude) - radar_object = ECRadar( - coordinates=(lat, lon), precip_type=config.get(CONF_PRECIP_TYPE) - ) + config[CONF_LATITUDE] = lat + config[CONF_LONGITUDE] = lon - add_devices( - [ECCamera(radar_object, config.get(CONF_NAME), config[CONF_LOOP])], True - ) + trigger_import(hass, config) -class ECCamera(Camera): +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a weather entity from a config_entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id]["radar_coordinator"] + async_add_entities([ECCamera(coordinator)]) + + +class ECCamera(CoordinatorEntity, Camera): """Implementation of an Environment Canada radar camera.""" - def __init__(self, radar_object, camera_name, is_loop): + def __init__(self, coordinator): """Initialize the camera.""" - super().__init__() + super().__init__(coordinator) + Camera.__init__(self) + + self.radar_object = coordinator.ec_data + self._attr_name = f"{coordinator.config_entry.title} Radar" + self._attr_unique_id = f"{coordinator.config_entry.unique_id}-radar" + self._attr_attribution = self.radar_object.metadata["attribution"] - self.radar_object = radar_object - self.camera_name = camera_name - self.is_loop = is_loop self.content_type = "image/gif" self.image = None - self.timestamp = None + self.observation_time = None def camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return bytes of camera image.""" - self.update() - return self.image - - @property - def name(self): - """Return the name of the camera.""" - if self.camera_name is not None: - return self.camera_name - return "Environment Canada Radar" + self.observation_time = self.radar_object.timestamp + return self.radar_object.image @property def extra_state_attributes(self): """Return the state attributes of the device.""" - return {ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_UPDATED: self.timestamp} - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update radar image.""" - if self.is_loop: - self.image = self.radar_object.get_loop() - else: - self.image = self.radar_object.get_latest_frame() - self.timestamp = self.radar_object.timestamp + return {ATTR_OBSERVATION_TIME: self.observation_time} diff --git a/homeassistant/components/environment_canada/config_flow.py b/homeassistant/components/environment_canada/config_flow.py new file mode 100644 index 00000000000..e1eda36c345 --- /dev/null +++ b/homeassistant/components/environment_canada/config_flow.py @@ -0,0 +1,101 @@ +"""Config flow for Environment Canada integration.""" +import logging +import xml.etree.ElementTree as et + +import aiohttp +from env_canada import ECWeather, ec_exc +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers import config_validation as cv + +from .const import CONF_LANGUAGE, CONF_STATION, CONF_TITLE, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(data): + """Validate the user input allows us to connect.""" + lat = data.get(CONF_LATITUDE) + lon = data.get(CONF_LONGITUDE) + station = data.get(CONF_STATION) + lang = data.get(CONF_LANGUAGE) + + weather_data = ECWeather( + station_id=station, + coordinates=(lat, lon), + language=lang.lower(), + ) + await weather_data.update() + + if lat is None or lon is None: + lat = weather_data.lat + lon = weather_data.lon + + return { + CONF_TITLE: weather_data.metadata.get("location"), + CONF_STATION: weather_data.station_id, + CONF_LATITUDE: lat, + CONF_LONGITUDE: lon, + } + + +class EnvironmentCanadaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Environment Canada weather.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(user_input) + except (et.ParseError, vol.MultipleInvalid, ec_exc.UnknownStationId): + errors["base"] = "bad_station_id" + except aiohttp.ClientConnectionError: + errors["base"] = "cannot_connect" + except aiohttp.ClientResponseError as err: + if err.status == 404: + errors["base"] = "bad_station_id" + else: + errors["base"] = "error_response" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + user_input[CONF_STATION] = info[CONF_STATION] + user_input[CONF_LATITUDE] = info[CONF_LATITUDE] + user_input[CONF_LONGITUDE] = info[CONF_LONGITUDE] + + # The combination of station and language are unique for all EC weather reporting + await self.async_set_unique_id( + f"{user_input[CONF_STATION]}-{user_input[CONF_LANGUAGE].lower()}" + ) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info[CONF_TITLE], data=user_input) + + data_schema = vol.Schema( + { + vol.Optional(CONF_STATION): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Required(CONF_LANGUAGE, default="English"): vol.In( + ["English", "French"] + ), + } + ) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) + + async def async_step_import(self, import_data): + """Import entry from configuration.yaml.""" + return await self.async_step_user(import_data) diff --git a/homeassistant/components/environment_canada/const.py b/homeassistant/components/environment_canada/const.py new file mode 100644 index 00000000000..16f7dc1cf99 --- /dev/null +++ b/homeassistant/components/environment_canada/const.py @@ -0,0 +1,8 @@ +"""Constants for EC component.""" + +ATTR_OBSERVATION_TIME = "observation_time" +ATTR_STATION = "station" +CONF_LANGUAGE = "language" +CONF_STATION = "station" +CONF_TITLE = "title" +DOMAIN = "environment_canada" diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 62c3e935d69..3a2ee1d8b8f 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,7 +2,8 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.2.5"], - "codeowners": ["@michaeldavie"], + "requirements": ["env_canada==0.5.14"], + "codeowners": ["@gwww", "@michaeldavie"], + "config_flow": true, "iot_class": "cloud_polling" } diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 3690703d8d2..0d33a166254 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -1,14 +1,11 @@ """Support for the Environment Canada weather service.""" -from datetime import datetime, timedelta import logging import re -from env_canada import ECData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_LATITUDE, CONF_LONGITUDE, @@ -16,26 +13,28 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import CoordinatorEntity -_LOGGER = logging.getLogger(__name__) +from . import trigger_import +from .const import ( + ATTR_OBSERVATION_TIME, + ATTR_STATION, + CONF_LANGUAGE, + CONF_STATION, + DOMAIN, +) -SCAN_INTERVAL = timedelta(minutes=10) - -ATTR_UPDATED = "updated" -ATTR_STATION = "station" ATTR_TIME = "alert time" -CONF_ATTRIBUTION = "Data provided by Environment Canada" -CONF_STATION = "station" -CONF_LANGUAGE = "language" +_LOGGER = logging.getLogger(__name__) def validate_station(station): """Check that the station ID is well-formed.""" if station is None: - return + return None if not re.fullmatch(r"[A-Z]{2}/s0000\d{3}", station): - raise vol.error.Invalid('Station ID must be of the form "XX/s0000###"') + raise vol.Invalid('Station ID must be of the form "XX/s0000###"') return station @@ -49,52 +48,47 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Environment Canada sensor.""" - - if config.get(CONF_STATION): - ec_data = ECData( - station_id=config[CONF_STATION], language=config.get(CONF_LANGUAGE) - ) - else: - lat = config.get(CONF_LATITUDE, hass.config.latitude) - lon = config.get(CONF_LONGITUDE, hass.config.longitude) - ec_data = ECData(coordinates=(lat, lon), language=config.get(CONF_LANGUAGE)) - - sensor_list = list(ec_data.conditions) + list(ec_data.alerts) - add_entities([ECSensor(sensor_type, ec_data) for sensor_type in sensor_list], True) + trigger_import(hass, config) -class ECSensor(SensorEntity): +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a weather entity from a config_entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id]["weather_coordinator"] + weather_data = coordinator.ec_data + + sensors = list(weather_data.conditions) + labels = [weather_data.conditions[sensor]["label"] for sensor in sensors] + alerts_list = list(weather_data.alerts) + labels = labels + [weather_data.alerts[sensor]["label"] for sensor in alerts_list] + sensors = sensors + alerts_list + + async_add_entities( + [ + ECSensor(coordinator, sensor, label) + for sensor, label in zip(sensors, labels) + ], + True, + ) + + +class ECSensor(CoordinatorEntity, SensorEntity): """Implementation of an Environment Canada sensor.""" - def __init__(self, sensor_type, ec_data): + def __init__(self, coordinator, sensor, label): """Initialize the sensor.""" - self.sensor_type = sensor_type - self.ec_data = ec_data + super().__init__(coordinator) + self.sensor_type = sensor + self.ec_data = coordinator.ec_data - self._unique_id = None - self._name = None - self._state = None + self._attr_attribution = self.ec_data.metadata["attribution"] + self._attr_name = f"{coordinator.config_entry.title} {label}" + self._attr_unique_id = f"{self.ec_data.metadata['location']}-{sensor}" self._attr = None self._unit = None self._device_class = None - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - @property def extra_state_attributes(self): """Return the state attributes of the device.""" @@ -110,32 +104,31 @@ class ECSensor(SensorEntity): """Return the class of this device, from component DEVICE_CLASSES.""" return self._device_class - def update(self): + @property + def native_value(self): """Update current conditions.""" - self.ec_data.update() - self.ec_data.conditions.update(self.ec_data.alerts) - - conditions = self.ec_data.conditions metadata = self.ec_data.metadata - sensor_data = conditions.get(self.sensor_type) + sensor_data = self.ec_data.conditions.get(self.sensor_type) + if not sensor_data: + sensor_data = self.ec_data.alerts.get(self.sensor_type) - self._unique_id = f"{metadata['location']}-{self.sensor_type}" self._attr = {} - self._name = sensor_data.get("label") value = sensor_data.get("value") if isinstance(value, list): - self._state = " | ".join([str(s.get("title")) for s in value])[:255] + state = " | ".join([str(s.get("title")) for s in value])[:255] self._attr.update( {ATTR_TIME: " | ".join([str(s.get("date")) for s in value])} ) elif self.sensor_type == "tendency": - self._state = str(value).capitalize() - elif value is not None and len(value) > 255: - self._state = value[:255] - _LOGGER.info("Value for %s truncated to 255 characters", self._unique_id) + state = str(value).capitalize() + elif isinstance(value, str) and len(value) > 255: + state = value[:255] + _LOGGER.info( + "Value for %s truncated to 255 characters", self._attr_unique_id + ) else: - self._state = value + state = value if sensor_data.get("unit") == "C" or self.sensor_type in ( "wind_chill", @@ -146,17 +139,11 @@ class ECSensor(SensorEntity): else: self._unit = sensor_data.get("unit") - timestamp = metadata.get("timestamp") - if timestamp: - updated_utc = datetime.strptime(timestamp, "%Y%m%d%H%M%S").isoformat() - else: - updated_utc = None - self._attr.update( { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_UPDATED: updated_utc, + ATTR_OBSERVATION_TIME: metadata.get("timestamp"), ATTR_LOCATION: metadata.get("location"), ATTR_STATION: metadata.get("station"), } ) + return state diff --git a/homeassistant/components/environment_canada/strings.json b/homeassistant/components/environment_canada/strings.json new file mode 100644 index 00000000000..49686cba123 --- /dev/null +++ b/homeassistant/components/environment_canada/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "title": "Environment Canada: weather location and language", + "description": "Either a station ID or latitude/longitude must be specified. The default latitude/longitude used are the values configured in your Home Assistant installation. The closest weather station to the coordinates will be used if specifying coordinates. If a station code is used it must follow the format: PP/code, where PP is the two-letter province and code is the station ID. The list of station IDs can be found here: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Weather information can be retrieved in either English or French.", + "data": { + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "station": "Weather station ID", + "language": "Weather information language" + } + } + }, + "error": { + "bad_station_id": "Station ID is invalid, missing, or not found in the station ID database", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "error_response": "Response from Environment Canada in error", + "too_many_attempts": "Connections to Environment Canada are rate limited; Try again in 60 seconds", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/environment_canada/translations/bg.json b/homeassistant/components/environment_canada/translations/bg.json new file mode 100644 index 00000000000..28c4730e5cd --- /dev/null +++ b/homeassistant/components/environment_canada/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "too_many_attempts": "\u0412\u0440\u044a\u0437\u043a\u0438\u0442\u0435 \u0441 Environment Canada \u0441\u0430 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438; \u041e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u0441\u043b\u0435\u0434 60 \u0441\u0435\u043a\u0443\u043d\u0434\u0438", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "language": "\u0415\u0437\u0438\u043a \u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0437\u0430 \u0432\u0440\u0435\u043c\u0435\u0442\u043e", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", + "station": "ID \u043d\u0430 \u043c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u043d\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/ca.json b/homeassistant/components/environment_canada/translations/ca.json new file mode 100644 index 00000000000..f847b2dc5ac --- /dev/null +++ b/homeassistant/components/environment_canada/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "L'ID d'estaci\u00f3 no \u00e9s v\u00e0lid, no est\u00e0 present o no es troba a la base de dades d'IDs d'estacions", + "cannot_connect": "Ha fallat la connexi\u00f3", + "error_response": "Resposta d'error d'Environment Canada", + "too_many_attempts": "Les connexions a Environment Canada estan limitades; torna-ho a provar d'aqu\u00ed a 60 segons", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "language": "Idioma de la informaci\u00f3 meteorol\u00f2gica", + "latitude": "Latitud", + "longitude": "Longitud", + "station": "ID d'estaci\u00f3 meteorol\u00f2gica" + }, + "description": "Cal especificar un identificador d'estaci\u00f3 o una latitud/longitud. La latitud/longitud que s'utilitza de manera predeterminada s'obt\u00e9 dels valors configurats a la instal\u00b7laci\u00f3 de Home Assistant. Si s'especifiquen coordenades, s'utilitzar\u00e0 l'estaci\u00f3 meteorol\u00f2gica m\u00e9s propera a aquestes coordenades. Si s'utilitza un codi d'estaci\u00f3, ha de ser amb el format: PP/codi, on PP s\u00f3n les dues lletres de prov\u00edncia i el codi \u00e9s l'identificador d'estaci\u00f3. Pots trobar la llista d'IDs d'estaci\u00f3 aqu\u00ed: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. La informaci\u00f3 meteorol\u00f2gica es pot obtenir en angl\u00e8s o franc\u00e8s.", + "title": "Environment Canada: ubicaci\u00f3 meteorol\u00f2gica i idioma" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/cs.json b/homeassistant/components/environment_canada/translations/cs.json new file mode 100644 index 00000000000..4eb6ccd754c --- /dev/null +++ b/homeassistant/components/environment_canada/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "language": "Jazyk informac\u00ed o po\u010das\u00ed", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "station": "ID meteorologick\u00e9 stanice" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/de.json b/homeassistant/components/environment_canada/translations/de.json new file mode 100644 index 00000000000..573e006aa3d --- /dev/null +++ b/homeassistant/components/environment_canada/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Die Stations-ID ist ung\u00fcltig, fehlt oder wurde in der Stations-ID-Datenbank nicht gefunden", + "cannot_connect": "Verbindung fehlgeschlagen", + "error_response": "Fehlerhafte Antwort von Environment Canada", + "too_many_attempts": "Verbindungen zu Environment Canada sind in ihrer Geschwindigkeit begrenzt; versuches in 60 Sekunden erneut.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "language": "Sprache der Wetterinformationen", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "station": "ID der Wetterstation" + }, + "description": "Es muss entweder eine Stations-ID oder der Breitengrad/L\u00e4ngengrad angegeben werden. Als Standardwerte f\u00fcr Breitengrad/L\u00e4ngengrad werden die in Ihrer Home Assistant-Installation konfigurierten Werte verwendet. Bei Angabe von Koordinaten wird die den Koordinaten am n\u00e4chsten gelegene Wetterstation verwendet. Wenn ein Stationscode verwendet wird, muss er dem Format entsprechen: PP/Code, wobei PP f\u00fcr die zweistellige Provinz und Code f\u00fcr die Stationskennung steht. Die Liste der Stations-IDs findest du hier: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Die Wetterinformationen k\u00f6nnen entweder in Englisch oder Franz\u00f6sisch abgerufen werden.", + "title": "Environment Canada: Wetterstandort und Sprache" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/en.json b/homeassistant/components/environment_canada/translations/en.json new file mode 100644 index 00000000000..94c0b947fa4 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Station ID is invalid, missing, or not found in the station ID database", + "cannot_connect": "Failed to connect", + "error_response": "Response from Environment Canada in error", + "too_many_attempts": "Connections to Environment Canada are rate limited; Try again in 60 seconds", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "language": "Weather information language", + "latitude": "Latitude", + "longitude": "Longitude", + "station": "Weather station ID" + }, + "description": "Either a station ID or latitude/longitude must be specified. The default latitude/longitude used are the values configured in your Home Assistant installation. The closest weather station to the coordinates will be used if specifying coordinates. If a station code is used it must follow the format: PP/code, where PP is the two-letter province and code is the station ID. The list of station IDs can be found here: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Weather information can be retrieved in either English or French.", + "title": "Environment Canada: weather location and language" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/et.json b/homeassistant/components/environment_canada/translations/et.json new file mode 100644 index 00000000000..af93b060144 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Jaama ID ei sobi, puudub v\u00f5i seda ei leitud jaamade ID andmebaasist", + "cannot_connect": "\u00dchendamine nurjus", + "error_response": "Kanada keskkonnaameti ekslik vastus", + "too_many_attempts": "\u00dchendus Kanada keskkonnaametiga on piiratud; proovi uuesti 60 sekundi p\u00e4rast", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "language": "Ilmateabe keel", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "station": "Ilmajaama ID" + }, + "description": "Tuleb m\u00e4\u00e4rata kas jaama ID v\u00f5i laiuskraad/pikkuskraad. Vaikimisi kasutatakse laiuskraadi/pikkuskraadi v\u00e4\u00e4rtusi, mis on konfigureeritud teie Home Assistant'i paigalduses. Koordinaatidele l\u00e4himat ilmajaama kasutatakse koordinaatide m\u00e4\u00e4ramisel. Kui kasutatakse jaama koodi, peab see j\u00e4rgima formaati: PP/kood, kus PP on kahet\u00e4heline provints ja kood on jaama ID. Jaama ID-de nimekiri on leitav siit: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Ilmateavet saab otsida kas inglise v\u00f5i prantsuse keeles.", + "title": "Kanada keskonnaamet: ilmateabe asukoht ja keel" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/he.json b/homeassistant/components/environment_canada/translations/he.json new file mode 100644 index 00000000000..9cb8e90c9f4 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/he.json @@ -0,0 +1,16 @@ +{ + "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": { + "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/environment_canada/translations/hu.json b/homeassistant/components/environment_canada/translations/hu.json new file mode 100644 index 00000000000..8a920274c1a --- /dev/null +++ b/homeassistant/components/environment_canada/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Az \u00e1llom\u00e1s azonos\u00edt\u00f3ja \u00e9rv\u00e9nytelen, hi\u00e1nyzik, vagy nem tal\u00e1lhat\u00f3 az \u00e1llom\u00e1s azonos\u00edt\u00f3 adatb\u00e1zisban.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "error_response": "Az Environment Canada hib\u00e1val v\u00e1laszolt", + "too_many_attempts": "Az Environment Canadahoz a kapcsol\u00f3d\u00e1sok sz\u00e1ma korl\u00e1tozva van; Pr\u00f3b\u00e1lja \u00fajra 60 m\u00e1sodperc m\u00falva", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "language": "Id\u0151j\u00e1r\u00e1si inform\u00e1ci\u00f3k nyelve", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "station": "Id\u0151j\u00e1r\u00e1s \u00e1llom\u00e1s ID-ja" + }, + "description": "Adja meg \u00e1llom\u00e1s ID-t vagy a sz\u00e9less\u00e9gi/hossz\u00fas\u00e1gi fokot. Az alap\u00e9rtelmezett f\u00f6ldrajzi sz\u00e9less\u00e9g/hossz\u00fas\u00e1g a Home Assistant telep\u00edt\u00e9s\u00e9n\u00e9l be\u00e1ll\u00edtott \u00e9rt\u00e9kek. Koordin\u00e1t\u00e1k megad\u00e1sa eset\u00e9n a koordin\u00e1t\u00e1khoz legk\u00f6zelebbi id\u0151j\u00e1r\u00e1si \u00e1llom\u00e1s ker\u00fcl felhaszn\u00e1l\u00e1sra. Ha \u00e1llom\u00e1sk\u00f3dot haszn\u00e1l, annak a k\u00f6vetkez\u0151 form\u00e1tumot kell k\u00f6vetnie: PP/k\u00f3d, ahol PP a k\u00e9tbet\u0171s tartom\u00e1ny, a k\u00f3d pedig az \u00e1llom\u00e1s azonos\u00edt\u00f3ja. Az \u00e1llom\u00e1sazonos\u00edt\u00f3k list\u00e1ja itt tal\u00e1lhat\u00f3: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Az id\u0151j\u00e1r\u00e1si inform\u00e1ci\u00f3k angol vagy francia nyelven k\u00e9rdezhet\u0151k le.", + "title": "Environment Canada: id\u0151j\u00e1r\u00e1s helysz\u00edne \u00e9s nyelv" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/it.json b/homeassistant/components/environment_canada/translations/it.json new file mode 100644 index 00000000000..f599eae7fe2 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "L'ID stazione non \u00e8 valido, mancante o non \u00e8 presente nel database degli ID stazione", + "cannot_connect": "Impossibile connettersi", + "error_response": "Risposta di Environment Canada in errore", + "too_many_attempts": "I collegamenti con Environment Canada sono limitati; Riprova tra 60 secondi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "language": "Lingua delle informazioni meteo", + "latitude": "Latitudine", + "longitude": "Logitudine", + "station": "ID stazione meteo" + }, + "description": "\u00c8 necessario specificare un ID stazione o latitudine/longitudine. La latitudine/longitudine predefinita utilizzata sono i valori configurati nell'installazione di Home Assistant. Se si specificano le coordinate, verr\u00e0 utilizzata la stazione meteorologica pi\u00f9 vicina alle coordinate. Se viene utilizzato un codice di stazione, deve seguire il formato: PP/codice, dove PP \u00e8 la provincia in due lettere e codice \u00e8 l'identificativo della stazione. L'elenco degli ID delle stazioni \u00e8 disponibile qui: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Le informazioni meteorologiche possono essere recuperate in inglese o francese.", + "title": "Environment Canada: posizione meteo e lingua" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/ja.json b/homeassistant/components/environment_canada/translations/ja.json new file mode 100644 index 00000000000..0d27b8acbe5 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "language": "\u6c17\u8c61\u60c5\u5831\u306e\u8a00\u8a9e", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", + "station": "\u30a6\u30a7\u30b6\u30fc\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/nl.json b/homeassistant/components/environment_canada/translations/nl.json new file mode 100644 index 00000000000..65d9aa9c906 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Station-ID is ongeldig, ontbreekt of is niet gevonden in de database met stations-ID's", + "cannot_connect": "Kan geen verbinding maken", + "error_response": "Antwoord van Environment Canada is fout", + "too_many_attempts": "Verbindingen met Environment Canada zijn gelimiteerd; Probeer opnieuw binnen 60 seconden", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "language": "Taal voor weerinformatie", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "station": "Weerstation-ID" + }, + "description": "Er moet een station-ID of een lengte-/breedtegraad worden opgegeven. De standaard gebruikte breedtegraad/lengtegraad zijn de waarden die in uw Home Assistant installatie zijn geconfigureerd. Als u co\u00f6rdinaten opgeeft, wordt het weerstation gebruikt dat zich het dichtst bij de co\u00f6rdinaten bevindt. Als een stationcode wordt gebruikt, moet deze het volgende formaat hebben PP/code, waarbij PP de provincie is met twee letters en code de ID van het station. De lijst van station ID's kan hier worden gevonden: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Weerinformatie kan worden opgevraagd in het Engels of Frans.", + "title": "Omgeving Canada: weerlocatie en taal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/no.json b/homeassistant/components/environment_canada/translations/no.json new file mode 100644 index 00000000000..8d0fb1f201b --- /dev/null +++ b/homeassistant/components/environment_canada/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Stasjons -ID er ugyldig, mangler eller finnes ikke i stasjons -ID -databasen", + "cannot_connect": "Tilkobling mislyktes", + "error_response": "Svar fra Environment Canada feilaktig", + "too_many_attempts": "Tilkoblinger til milj\u00f8 Canada er takstbegrenset; Pr\u00f8v igjen om 60 sekunder", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "language": "Spr\u00e5k for v\u00e6rinformasjon", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "station": "Id for v\u00e6rstasjon" + }, + "description": "Enten en stasjons -ID eller breddegrad/lengdegrad m\u00e5 spesifiseres. Standard breddegrad/lengdegrad som brukes er verdiene som er konfigurert i Home Assistant -installasjonen. Den n\u00e6rmeste v\u00e6rstasjonen til koordinatene vil bli brukt hvis du angir koordinater. Hvis en stasjonskode brukes, m\u00e5 den f\u00f8lge formatet: PP/kode, hvor PP er provinsen p\u00e5 to bokstaver og koden er stasjons-ID. Listen over stasjons -ID -er finner du her: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. V\u00e6rinformasjon kan hentes p\u00e5 enten engelsk eller fransk.", + "title": "Milj\u00f8 Canada: v\u00e6rsted og spr\u00e5k" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/pl.json b/homeassistant/components/environment_canada/translations/pl.json new file mode 100644 index 00000000000..b840de8a10e --- /dev/null +++ b/homeassistant/components/environment_canada/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "language": "J\u0119zyk informacji pogodowych", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "station": "Identyfikator stacji pogodowej" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/ru.json b/homeassistant/components/environment_canada/translations/ru.json new file mode 100644 index 00000000000..26c0108ed3a --- /dev/null +++ b/homeassistant/components/environment_canada/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d, \u043b\u0438\u0431\u043e \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "error_response": "\u041e\u0442\u0432\u0435\u0442 \u043e\u0442 Environment Canada \u043f\u043e \u043e\u0448\u0438\u0431\u043a\u0435.", + "too_many_attempts": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Environment Canada \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043e. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u0447\u0435\u0440\u0435\u0437 60 \u0441\u0435\u043a\u0443\u043d\u0434.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "language": "\u042f\u0437\u044b\u043a, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0433\u043e\u0434\u0435", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "station": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0438" + }, + "description": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043a\u0430\u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0438, \u043b\u0438\u0431\u043e \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0448\u0438\u0440\u043e\u0442\u044b \u0438 \u0434\u043e\u043b\u0433\u043e\u0442\u044b, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0412\u0430\u0448\u0435\u0433\u043e Home Assistant. \u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0430\u044f \u043a \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u044f. \u0415\u0441\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0438, \u043e\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u0444\u043e\u0440\u043c\u0430\u0442\u0443: PP/\u043a\u043e\u0434, \u0433\u0434\u0435 PP \u2014 \u044d\u0442\u043e \u0438\u043d\u0434\u0435\u043a\u0441 \u043f\u0440\u043e\u0432\u0438\u043d\u0446\u0438\u0438, \u0430 \u043a\u043e\u0434 \u2014 \u044d\u0442\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0438. \u0421\u043f\u0438\u0441\u043e\u043a \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u043e\u0432 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0437\u0434\u0435\u0441\u044c: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv.\n\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u043f\u043e\u0433\u043e\u0434\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0430 \u0430\u043d\u0433\u043b\u0438\u0439\u0441\u043a\u043e\u043c \u0438\u043b\u0438 \u0444\u0440\u0430\u043d\u0446\u0443\u0437\u0441\u043a\u043e\u043c \u044f\u0437\u044b\u043a\u0430\u0445.", + "title": "Environment Canada: \u043f\u043e\u0433\u043e\u0434\u0430, \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438 \u044f\u0437\u044b\u043a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/tr.json b/homeassistant/components/environment_canada/translations/tr.json new file mode 100644 index 00000000000..afd8eb43d46 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "bad_station_id": "\u0130stasyon Kimli\u011fi ge\u00e7ersiz, eksik veya istasyon kimli\u011fi veritaban\u0131nda bulunamad\u0131", + "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "language": "Hava durumu bilgisi dili", + "latitude": "Enlem", + "longitude": "Boylam", + "station": "Hava istasyonu ID" + }, + "description": "Bir istasyon kimli\u011fi veya enlem/boylam belirtilmelidir. Kullan\u0131lan varsay\u0131lan enlem/boylam, Home Assistant kurulumunuzda yap\u0131land\u0131r\u0131lan de\u011ferlerdir. Koordinatlar belirtilirse, koordinatlara en yak\u0131n meteoroloji istasyonu kullan\u0131lacakt\u0131r. Bir istasyon kodu kullan\u0131l\u0131yorsa, \u015fu bi\u00e7imde olmal\u0131d\u0131r: PP/kod, burada PP iki harfli ildir ve kod istasyon kimli\u011fidir. \u0130stasyon kimliklerinin listesi burada bulunabilir: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Hava durumu bilgileri \u0130ngilizce veya Frans\u0131zca olarak al\u0131nabilir." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/zh-Hant.json b/homeassistant/components/environment_canada/translations/zh-Hant.json new file mode 100644 index 00000000000..59fe99e8ead --- /dev/null +++ b/homeassistant/components/environment_canada/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "\u6c23\u8c61\u7ad9 ID \u7121\u6548\u3001\u907a\u5931\u6216\u8cc7\u6599\u5eab\u4e2d\u627e\u4e0d\u5230\u8a72 ID", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "error_response": "\u4f86\u81ea Environment Canada \u56de\u8986\u932f\u8aa4", + "too_many_attempts": "\u8207 Environment Canada \u9023\u7dda\u6b21\u6578\u70ba\u6709\u9650\u6b21\u6578\uff1b\u8acb\u65bc 60 \u79d2\u5f8c\u518d\u8a66\u4e00\u6b21", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "language": "\u6c23\u8c61\u8cc7\u8a0a\u8a9e\u8a00", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "station": "\u6c23\u8c61\u7ad9 ID" + }, + "description": "\u5fc5\u9808\u6307\u5b9a\u6c23\u8c61\u7ad9 ID \u6216\u7d93\u5ea6/\u7def\u5ea6\u3002\u5c07\u4f7f\u7528 Home Assistant \u5b89\u88dd\u4e2d\u8a2d\u5b9a\u4e4b\u7d93\u5ea6/\u7def\u5ea6\u70ba\u9810\u8a2d\u503c\uff0c\u4e26\u4f7f\u7528\u6700\u9760\u8fd1\u7684\u6c23\u8c61\u7ad9\u8cc7\u6599\u3002\u5047\u5982\u4f7f\u7528\u6c23\u8c61\u7ad9\u4ee3\u78bc\u5247\u5fc5\u9808\u8ddf\u96a8\u4ee5\u4e0b\u683c\u5f0f\uff1aPP/\u4ee3\u78bc\uff0cPP \u70ba\u5169\u4f4d\u5b57\u6bcd\u8868\u793a\u7701/\u5dde\u3001\u800c\u4ee3\u78bc\u5247\u70ba\u6c23\u8c61\u7ad9 ID\u3002\u53ef\u4ee5\u65bc\u6b64\u8655\u627e\u5230\u6c23\u8c61\u7ad9 ID \u5217\u8868\uff1ahttps://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv\u3002\u6c23\u8c61\u8cc7\u8a0a\u5247\u53ef\u8a2d\u5b9a\u70ba\u82f1\u6587\u6216\u6cd5\u6587\u3002", + "title": "Environment Canada\uff1a\u6c23\u8c61\u7ad9\u4f4d\u7f6e\u8207\u8a9e\u8a00" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index cf24146da14..5231e95e2bc 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -1,8 +1,9 @@ """Platform for retrieving meteorological data from Environment Canada.""" +from __future__ import annotations + import datetime import re -from env_canada import ECData import voluptuous as vol from homeassistant.components.weather import ( @@ -28,19 +29,21 @@ from homeassistant.components.weather import ( ) from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt +from . import trigger_import +from .const import CONF_STATION, DOMAIN + CONF_FORECAST = "forecast" -CONF_ATTRIBUTION = "Data provided by Environment Canada" -CONF_STATION = "station" def validate_station(station): """Check that the station ID is well-formed.""" if station is None: - return + return None if not re.fullmatch(r"[A-Z]{2}/s0000\d{3}", station): - raise vol.error.Invalid('Station ID must be of the form "XX/s0000###"') + raise vol.Invalid('Station ID must be of the form "XX/s0000###"') return station @@ -72,45 +75,41 @@ ICON_CONDITION_MAP = { } -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entries, discovery_info=None): """Set up the Environment Canada weather.""" - if config.get(CONF_STATION): - ec_data = ECData(station_id=config[CONF_STATION]) - else: - lat = config.get(CONF_LATITUDE, hass.config.latitude) - lon = config.get(CONF_LONGITUDE, hass.config.longitude) - ec_data = ECData(coordinates=(lat, lon)) - - add_devices([ECWeather(ec_data, config)]) + trigger_import(hass, config) -class ECWeather(WeatherEntity): +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a weather entity from a config_entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id]["weather_coordinator"] + async_add_entities([ECWeather(coordinator, False), ECWeather(coordinator, True)]) + + +class ECWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" - def __init__(self, ec_data, config): + def __init__(self, coordinator, hourly): """Initialize Environment Canada weather.""" - self.ec_data = ec_data - self.platform_name = config.get(CONF_NAME) - self.forecast_type = config[CONF_FORECAST] - - @property - def attribution(self): - """Return the attribution.""" - return CONF_ATTRIBUTION - - @property - def name(self): - """Return the name of the weather entity.""" - if self.platform_name: - return self.platform_name - return self.ec_data.metadata.get("location") + super().__init__(coordinator) + self.ec_data = coordinator.ec_data + self._attr_attribution = self.ec_data.metadata["attribution"] + self._attr_name = ( + f"{coordinator.config_entry.title}{' Hourly' if hourly else ''}" + ) + self._attr_unique_id = ( + f"{coordinator.config_entry.unique_id}{'-hourly' if hourly else '-daily'}" + ) + self._hourly = hourly @property def temperature(self): """Return the temperature.""" if self.ec_data.conditions.get("temperature", {}).get("value"): return float(self.ec_data.conditions["temperature"]["value"]) - if self.ec_data.hourly_forecasts[0].get("temperature"): + if self.ec_data.hourly_forecasts and self.ec_data.hourly_forecasts[0].get( + "temperature" + ): return float(self.ec_data.hourly_forecasts[0]["temperature"]) return None @@ -161,7 +160,9 @@ class ECWeather(WeatherEntity): if self.ec_data.conditions.get("icon_code", {}).get("value"): icon_code = self.ec_data.conditions["icon_code"]["value"] - elif self.ec_data.hourly_forecasts[0].get("icon_code"): + elif self.ec_data.hourly_forecasts and self.ec_data.hourly_forecasts[0].get( + "icon_code" + ): icon_code = self.ec_data.hourly_forecasts[0]["icon_code"] if icon_code: @@ -171,19 +172,16 @@ class ECWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - return get_forecast(self.ec_data, self.forecast_type) - - def update(self): - """Get the latest data from Environment Canada.""" - self.ec_data.update() + return get_forecast(self.ec_data, self._hourly) -def get_forecast(ec_data, forecast_type): +def get_forecast(ec_data, hourly): """Build the forecast array.""" forecast_array = [] - if forecast_type == "daily": - half_days = ec_data.daily_forecasts + if not hourly: + if not (half_days := ec_data.daily_forecasts): + return None today = { ATTR_FORECAST_TIME: dt.now().isoformat(), @@ -231,15 +229,11 @@ def get_forecast(ec_data, forecast_type): } ) - elif forecast_type == "hourly": + else: for hour in ec_data.hourly_forecasts: forecast_array.append( { - ATTR_FORECAST_TIME: datetime.datetime.strptime( - hour["period"], "%Y%m%d%H%M%S" - ) - .replace(tzinfo=dt.UTC) - .isoformat(), + ATTR_FORECAST_TIME: hour["period"], ATTR_FORECAST_TEMP: int(hour["temperature"]), ATTR_FORECAST_CONDITION: icon_code_to_condition( int(hour["icon_code"]) diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 787677a6605..022c91c96f3 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -161,8 +161,7 @@ class EphEmberThermostat(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return if self._hot_water: diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index e60df7dc8bc..036b8df7ca9 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 1fd0b7f6e70..5223a9663d0 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -1,4 +1,6 @@ """Support for Epson projector.""" +from __future__ import annotations + import logging from epson_projector.const import ( @@ -39,6 +41,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from .const import ATTR_CMODE, DOMAIN, SERVICE_SELECT_CMODE @@ -137,17 +140,17 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): self._state = STATE_OFF @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Get attributes about the device.""" if not self._unique_id: return None - return { - "identifiers": {(DOMAIN, self._unique_id)}, - "manufacturer": "Epson", - "name": "Epson projector", - "model": "Epson", - "via_hub": (DOMAIN, self._unique_id), - } + return DeviceInfo( + identifiers={(DOMAIN, self._unique_id)}, + manufacturer="Epson", + model="Epson", + name="Epson projector", + via_device=(DOMAIN, self._unique_id), + ) @property def name(self): diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index f803c9c0bd5..b7c39ec996c 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -24,6 +24,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac _LOGGER = logging.getLogger(__name__) @@ -82,6 +83,7 @@ class EQ3BTSmartThermostat(ClimateEntity): """Initialize the thermostat.""" # We want to avoid name clash with this module. self._name = _name + self._mac = _mac self._thermostat = eq3.Thermostat(_mac) @property @@ -121,8 +123,7 @@ class EQ3BTSmartThermostat(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self._thermostat.target_temperature = temperature @@ -183,6 +184,11 @@ class EQ3BTSmartThermostat(ClimateEntity): """ return list(HA_TO_EQ_PRESET) + @property + def unique_id(self) -> str: + """Return the MAC address of the thermostat.""" + return format_mac(self._mac) + def set_preset_mode(self, preset_mode): """Set new preset mode.""" if preset_mode == PRESET_NONE: diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index de301e0c1bb..2825e036884 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -1,8 +1,6 @@ """Support for esphome devices.""" from __future__ import annotations -import asyncio -from collections.abc import Awaitable from dataclasses import dataclass, field import functools import logging @@ -15,16 +13,17 @@ from aioesphomeapi import ( APIIntEnum, APIVersion, DeviceInfo as EsphomeDeviceInfo, + EntityCategory, EntityInfo, EntityState, HomeassistantServiceCall, InvalidEncryptionKeyAPIError, + ReconnectLogic, RequiresEncryptionAPIError, UserService, UserServiceArgType, ) import voluptuous as vol -from zeroconf import DNSPointer, RecordUpdate, RecordUpdateListener, Zeroconf from homeassistant import const from homeassistant.components import zeroconf @@ -34,6 +33,8 @@ from homeassistant.const import ( CONF_MODE, CONF_PASSWORD, CONF_PORT, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback @@ -119,7 +120,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: zeroconf_instance = await zeroconf.async_get_instance(hass) cli = APIClient( - hass.loop, host, port, password, @@ -259,7 +259,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _send_home_assistant_state(entity_id, attribute, hass.states.get(entity_id)) ) - async def on_login() -> None: + async def on_connect() -> None: """Subscribe to states and list entities on successful API login.""" nonlocal device_id try: @@ -285,8 +285,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Re-connection logic will trigger after this await cli.disconnect() + async def on_disconnect() -> None: + """Run disconnect callbacks on API disconnect.""" + for disconnect_cb in entry_data.disconnect_callbacks: + disconnect_cb() + entry_data.disconnect_callbacks = [] + entry_data.available = False + entry_data.async_update_device_state(hass) + + async def on_connect_error(err: Exception) -> None: + """Start reauth flow if appropriate connect error type.""" + if isinstance(err, (RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError)): + entry.async_start_reauth(hass) + reconnect_logic = ReconnectLogic( - hass, cli, entry, host, on_login, zeroconf_instance + client=cli, + on_connect=on_connect, + on_disconnect=on_disconnect, + zeroconf_instance=zeroconf_instance, + name=host, + on_connect_error=on_connect_error, ) async def complete_setup() -> None: @@ -302,258 +320,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -class ReconnectLogic(RecordUpdateListener): - """Reconnectiong logic handler for ESPHome config entries. - - Contains two reconnect strategies: - - Connect with increasing time between connection attempts. - - Listen to zeroconf mDNS records, if any records are found for this device, try reconnecting immediately. - """ - - def __init__( - self, - hass: HomeAssistant, - cli: APIClient, - entry: ConfigEntry, - host: str, - on_login: Callable[[], Awaitable[None]], - zc: Zeroconf, - ) -> None: - """Initialize ReconnectingLogic.""" - self._hass = hass - self._cli = cli - self._entry = entry - self._host = host - self._on_login = on_login - self._zc = zc - # Flag to check if the device is connected - self._connected = True - self._connected_lock = asyncio.Lock() - self._zc_lock = asyncio.Lock() - self._zc_listening = False - # Event the different strategies use for issuing a reconnect attempt. - self._reconnect_event = asyncio.Event() - # The task containing the infinite reconnect loop while running - self._loop_task: asyncio.Task[None] | None = None - # How many reconnect attempts have there been already, used for exponential wait time - self._tries = 0 - self._tries_lock = asyncio.Lock() - # Track the wait task to cancel it on HA shutdown - self._wait_task: asyncio.Task[None] | None = None - self._wait_task_lock = asyncio.Lock() - - @property - def _entry_data(self) -> RuntimeEntryData | None: - domain_data = DomainData.get(self._hass) - try: - return domain_data.get_entry_data(self._entry) - except KeyError: - return None - - async def _on_disconnect(self) -> None: - """Log and issue callbacks when disconnecting.""" - if self._entry_data is None: - return - # This can happen often depending on WiFi signal strength. - # So therefore all these connection warnings are logged - # as infos. The "unavailable" logic will still trigger so the - # user knows if the device is not connected. - _LOGGER.info("Disconnected from ESPHome API for %s", self._host) - - # Run disconnect hooks - for disconnect_cb in self._entry_data.disconnect_callbacks: - disconnect_cb() - self._entry_data.disconnect_callbacks = [] - self._entry_data.available = False - self._entry_data.async_update_device_state(self._hass) - await self._start_zc_listen() - - # Reset tries - async with self._tries_lock: - self._tries = 0 - # Connected needs to be reset before the reconnect event (opposite order of check) - async with self._connected_lock: - self._connected = False - self._reconnect_event.set() - - async def _wait_and_start_reconnect(self) -> None: - """Wait for exponentially increasing time to issue next reconnect event.""" - async with self._tries_lock: - tries = self._tries - # If not first re-try, wait and print message - # Cap wait time at 1 minute. This is because while working on the - # device (e.g. soldering stuff), users don't want to have to wait - # a long time for their device to show up in HA again (this was - # mentioned a lot in early feedback) - tries = min(tries, 10) # prevent OverflowError - wait_time = int(round(min(1.8 ** tries, 60.0))) - if tries == 1: - _LOGGER.info("Trying to reconnect to %s in the background", self._host) - _LOGGER.debug("Retrying %s in %d seconds", self._host, wait_time) - await asyncio.sleep(wait_time) - async with self._wait_task_lock: - self._wait_task = None - self._reconnect_event.set() - - async def _try_connect(self) -> None: - """Try connecting to the API client.""" - async with self._tries_lock: - tries = self._tries - self._tries += 1 - - try: - await self._cli.connect(on_stop=self._on_disconnect, login=True) - except APIConnectionError as error: - if isinstance( - error, (RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError) - ): - self._entry.async_start_reauth(self._hass) - - level = logging.WARNING if tries == 0 else logging.DEBUG - _LOGGER.log( - level, - "Can't connect to ESPHome API for %s (%s): %s", - self._entry.unique_id, - self._host, - error, - ) - await self._start_zc_listen() - # Schedule re-connect in event loop in order not to delay HA - # startup. First connect is scheduled in tracked tasks. - async with self._wait_task_lock: - # Allow only one wait task at a time - # can happen if mDNS record received while waiting, then use existing wait task - if self._wait_task is not None: - return - - self._wait_task = self._hass.loop.create_task( - self._wait_and_start_reconnect() - ) - else: - _LOGGER.info("Successfully connected to %s", self._host) - async with self._tries_lock: - self._tries = 0 - async with self._connected_lock: - self._connected = True - await self._stop_zc_listen() - self._hass.async_create_task(self._on_login()) - - async def _reconnect_once(self) -> None: - # Wait and clear reconnection event - await self._reconnect_event.wait() - self._reconnect_event.clear() - - # If in connected state, do not try to connect again. - async with self._connected_lock: - if self._connected: - return - - # Check if the entry got removed or disabled, in which case we shouldn't reconnect - if not DomainData.get(self._hass).is_entry_loaded(self._entry): - # When removing/disconnecting manually - return - - device_registry = self._hass.helpers.device_registry.async_get(self._hass) - devices = dr.async_entries_for_config_entry( - device_registry, self._entry.entry_id - ) - for device in devices: - # There is only one device in ESPHome - if device.disabled: - # Don't attempt to connect if it's disabled - return - - await self._try_connect() - - async def _reconnect_loop(self) -> None: - while True: - try: - await self._reconnect_once() - except asyncio.CancelledError: # pylint: disable=try-except-raise - raise - except Exception: # pylint: disable=broad-except - _LOGGER.error("Caught exception while reconnecting", exc_info=True) - - async def start(self) -> None: - """Start the reconnecting logic background task.""" - # Create reconnection loop outside of HA's tracked tasks in order - # not to delay startup. - self._loop_task = self._hass.loop.create_task(self._reconnect_loop()) - - async with self._connected_lock: - self._connected = False - self._reconnect_event.set() - - async def stop(self) -> None: - """Stop the reconnecting logic background task. Does not disconnect the client.""" - if self._loop_task is not None: - self._loop_task.cancel() - self._loop_task = None - async with self._wait_task_lock: - if self._wait_task is not None: - self._wait_task.cancel() - self._wait_task = None - await self._stop_zc_listen() - - async def _start_zc_listen(self) -> None: - """Listen for mDNS records. - - This listener allows us to schedule a reconnect as soon as a - received mDNS record indicates the node is up again. - """ - async with self._zc_lock: - if not self._zc_listening: - self._zc.async_add_listener(self, None) - self._zc_listening = True - - async def _stop_zc_listen(self) -> None: - """Stop listening for zeroconf updates.""" - async with self._zc_lock: - if self._zc_listening: - self._zc.async_remove_listener(self) - self._zc_listening = False - - @callback - def stop_callback(self) -> None: - """Stop as an async callback function.""" - self._hass.async_create_task(self.stop()) - - def async_update_records( - self, zc: Zeroconf, now: float, records: list[RecordUpdate] - ) -> None: - """Listen to zeroconf updated mDNS records. - - This is a mDNS record from the device and could mean it just woke up. - """ - # Check if already connected, no lock needed for this access and - # bail if either the entry was already teared down or we haven't received device info yet - if ( - self._connected - or self._reconnect_event.is_set() - or self._entry_data is None - or self._entry_data.device_info is None - ): - return - filter_alias = f"{self._entry_data.device_info.name}._esphomelib._tcp.local." - - for record_update in records: - # We only consider PTR records and match using the alias name - if ( - not isinstance(record_update.new, DNSPointer) - or record_update.new.alias != filter_alias - ): - continue - - # Tell reconnection logic to retry connection attempt now (even before reconnect timer finishes) - _LOGGER.debug( - "%s: Triggering reconnect because of received mDNS record %s", - self._host, - record_update.new, - ) - self._reconnect_event.set() - return - - async def _async_setup_device_registry( hass: HomeAssistant, entry: ConfigEntry, device_info: EsphomeDeviceInfo ) -> str: @@ -561,9 +327,13 @@ async def _async_setup_device_registry( sw_version = device_info.esphome_version if device_info.compilation_time: sw_version += f" ({device_info.compilation_time})" - device_registry = await dr.async_get_registry(hass) + configuration_url = None + if device_info.webserver_port > 0: + configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}" + device_registry = dr.async_get(hass) device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, + configuration_url=configuration_url, connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, name=device_info.name, manufacturer="espressif", @@ -874,6 +644,18 @@ class EsphomeEnumMapper(Generic[_EnumT, _ValT]): return self._inverse[value] +ICON_SCHEMA = vol.Schema(cv.icon) + + +ENTITY_CATEGORIES: EsphomeEnumMapper[EntityCategory, str | None] = EsphomeEnumMapper( + { + EntityCategory.NONE: None, + EntityCategory.CONFIG: ENTITY_CATEGORY_CONFIG, + EntityCategory.DIAGNOSTIC: ENTITY_CATEGORY_DIAGNOSTIC, + } +) + + class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" @@ -989,15 +771,23 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): @property def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" - return { - "connections": {(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)} - } + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)} + ) @property def name(self) -> str: """Return the name of the entity.""" return self._static_info.name + @property + def icon(self) -> str | None: + """Return the icon.""" + if not self._static_info.icon: + return None + + return cast(str, ICON_SCHEMA(self._static_info.icon)) + @property def should_poll(self) -> bool: """Disable polling.""" @@ -1007,3 +797,10 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return not self._static_info.disabled_by_default + + @property + def entity_category(self) -> str | None: + """Return the category of the entity, if any.""" + if not self._static_info.entity_category: + return None + return ENTITY_CATEGORIES.from_esphome(self._static_info.entity_category) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 7a7e45c440b..a794404b685 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -260,7 +260,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): assert self._host is not None assert self._port is not None cli = APIClient( - self.hass.loop, self._host, self._port, "", @@ -292,7 +291,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): assert self._host is not None assert self._port is not None cli = APIClient( - self.hass.loop, self._host, self._port, self._password, diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index b8fe4bd74c7..eb5e258f079 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -281,8 +281,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): def color_mode(self) -> str | None: """Return the color mode of the light.""" if not self._supports_color_mode: - supported = self.supported_color_modes - if not supported: + if not (supported := self.supported_color_modes): return None return next(iter(supported)) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 307227be944..247c78abb92 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==9.1.5"], + "requirements": ["aioesphomeapi==10.2.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 1a90cdbeb24..c8baa1f112e 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -2,21 +2,16 @@ from __future__ import annotations import math -from typing import cast 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, @@ -42,13 +37,6 @@ async def async_setup_entry( class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): """A number implementation for esphome.""" - @property - def icon(self) -> str | None: - """Return the icon.""" - if not self._static_info.icon: - return None - return cast(str, ICON_SCHEMA(self._static_info.icon)) - @property def min_value(self) -> float: """Return the minimum value.""" diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index 6ba6ba4c594..f3bfcb982ea 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -1,21 +1,15 @@ """Support for esphome selects.""" from __future__ import annotations -from typing import cast - from aioesphomeapi import SelectInfo, SelectState -import voluptuous as vol from homeassistant.components.select import SelectEntity 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, @@ -41,13 +35,6 @@ async def async_setup_entry( class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity): """A select implementation for esphome.""" - @property - def icon(self) -> str | None: - """Return the icon.""" - if not self._static_info.icon: - return None - return cast(str, ICON_SCHEMA(self._static_info.icon)) - @property def options(self) -> list[str]: """Return a set of selectable options.""" diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index c0ea9f0f9c5..b2758c91b68 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations import math -from typing import cast from aioesphomeapi import ( SensorInfo, @@ -12,7 +11,6 @@ from aioesphomeapi import ( TextSensorState, ) from aioesphomeapi.model import LastResetType -import voluptuous as vol from homeassistant.components.sensor import ( DEVICE_CLASS_TIMESTAMP, @@ -23,7 +21,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt @@ -34,8 +31,6 @@ from . import ( platform_async_setup_entry, ) -ICON_SCHEMA = vol.Schema(cv.icon) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -77,13 +72,6 @@ _STATE_CLASSES: EsphomeEnumMapper[SensorStateClass, str | None] = EsphomeEnumMap class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): """A sensor implementation for esphome.""" - @property - def icon(self) -> str | None: - """Return the icon.""" - if not self._static_info.icon or self._static_info.device_class: - return None - return cast(str, ICON_SCHEMA(self._static_info.icon)) - @property def force_update(self) -> bool: """Return if this sensor should force a state update.""" @@ -133,11 +121,6 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity): """A text sensor implementation for ESPHome.""" - @property - def icon(self) -> str: - """Return the icon.""" - return self._static_info.icon - @esphome_state_property def native_value(self) -> str | None: """Return the state of the entity.""" diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 218cd3905b0..a8f0febf5b0 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -35,11 +35,6 @@ async def async_setup_entry( class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): """A switch implementation for ESPHome.""" - @property - def icon(self) -> str: - """Return the icon.""" - return self._static_info.icon - @property def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" diff --git a/homeassistant/components/esphome/translations/bg.json b/homeassistant/components/esphome/translations/bg.json index 1a92f62cbb6..699a993403f 100644 --- a/homeassistant/components/esphome/translations/bg.json +++ b/homeassistant/components/esphome/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "ESP \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 ESP. \u041c\u043e\u043b\u044f, \u0443\u0432\u0435\u0440\u0435\u0442\u0435 \u0441\u0435, \u0447\u0435 \u0432\u0430\u0448\u0438\u044f\u0442 YAML \u0444\u0430\u0439\u043b \u0441\u044a\u0434\u044a\u0440\u0436\u0430 \u0440\u0435\u0434 \"api:\".", @@ -19,6 +20,17 @@ "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u0435 ESPHome \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e ` {name} ` \u043a\u044a\u043c Home Assistant?", "title": "\u041e\u0442\u043a\u0440\u0438\u0442\u043e \u0435 ESPHome \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, + "encryption_key": { + "data": { + "noise_psk": "\u041a\u043b\u044e\u0447 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043a\u043b\u044e\u0447\u0430 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435, \u043a\u043e\u0439\u0442\u043e \u0441\u0442\u0435 \u0437\u0430\u0434\u0430\u043b\u0438 \u0432\u044a\u0432 \u0432\u0430\u0448\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0437\u0430 {name}." + }, + "reauth_confirm": { + "data": { + "noise_psk": "\u041a\u043b\u044e\u0447 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435" + } + }, "user": { "data": { "host": "\u0410\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index e65577f055e..17af0e57d26 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { - "connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a YAML f\u00e1jl tartalmaz egy \"api:\" sort.", + "connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a YAML konfigur\u00e1ci\u00f3 tartalmaz egy \"api:\" sort.", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_psk": "Az adat\u00e1tviteli titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen. K\u00e9rj\u00fck, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy megegyezik a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151vel.", "resolve_error": "Az ESP c\u00edme nem oldhat\u00f3 fel. Ha a hiba tov\u00e1bbra is fenn\u00e1ll, k\u00e9rem, \u00e1ll\u00edtson be egy statikus IP-c\u00edmet: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" @@ -17,23 +17,23 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rem, adja meg a konfigur\u00e1ci\u00f3ban {name} n\u00e9vhez be\u00e1ll\u00edtott jelsz\u00f3t." + "description": "K\u00e9rem, adja meg a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151, {name} jelszav\u00e1t." }, "discovery_confirm": { "description": "Szeretn\u00e9 hozz\u00e1adni `{name}` ESPHome csom\u00f3pontot Home Assistanthoz?", - "title": "Felfedezett ESPHome csom\u00f3pont" + "title": "ESPHome csom\u00f3pont felfedezve" }, "encryption_key": { "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "K\u00e9rj\u00fck, adja meg a {name} konfigur\u00e1ci\u00f3j\u00e1ban be\u00e1ll\u00edtott titkos\u00edt\u00e1si kulcsot." + "description": "K\u00e9rj\u00fck, adja meg a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151, {name} titkos\u00edt\u00e1si kulcs\u00e1t." }, "reauth_confirm": { "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "{name} ESPHome eszk\u00f6z enged\u00e9lyezte az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rj\u00fck, adja meg az aktu\u00e1lis kulcsot." + "description": "{name} ESPHome v\u00e9gpont aktiv\u00e1lta az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rj\u00fck, adja meg az aktu\u00e1lis kulcsot." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index 7619f29ad64..2fa5f37ff18 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -2,7 +2,8 @@ "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", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.", diff --git a/homeassistant/components/essent/__init__.py b/homeassistant/components/essent/__init__.py deleted file mode 100644 index 42e867c6d21..00000000000 --- a/homeassistant/components/essent/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The Essent component.""" diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json deleted file mode 100644 index d136cae43a9..00000000000 --- a/homeassistant/components/essent/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "essent", - "name": "Essent", - "documentation": "https://www.home-assistant.io/integrations/essent", - "requirements": ["PyEssent==0.14"], - "codeowners": ["@TheLastProject"], - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py deleted file mode 100644 index 42a4c1c399b..00000000000 --- a/homeassistant/components/essent/sensor.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Support for Essent API.""" -from __future__ import annotations - -from datetime import timedelta - -from pyessent import PyEssent -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, ENERGY_KILO_WATT_HOUR -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle - -SCAN_INTERVAL = timedelta(hours=1) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} -) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Essent platform.""" - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - - essent = EssentBase(username, password) - meters = [] - for meter in essent.retrieve_meters(): - data = essent.retrieve_meter_data(meter) - for tariff in data["values"]["LVR"]: - meters.append( - EssentMeter( - essent, - meter, - data["type"], - tariff, - data["values"]["LVR"][tariff]["unit"], - ) - ) - - if not meters: - hass.components.persistent_notification.create( - "Couldn't find any meter readings. " - "Please ensure Verbruiks Manager is enabled in Mijn Essent " - "and at least one reading has been logged to Meterstanden.", - title="Essent", - notification_id="essent_notification", - ) - return - - add_devices(meters, True) - - -class EssentBase: - """Essent Base.""" - - def __init__(self, username, password): - """Initialize the Essent API.""" - self._username = username - self._password = password - self._meter_data = {} - - self.update() - - def retrieve_meters(self): - """Retrieve the list of meters.""" - return self._meter_data.keys() - - def retrieve_meter_data(self, meter): - """Retrieve the data for this meter.""" - return self._meter_data[meter] - - @Throttle(timedelta(minutes=30)) - def update(self): - """Retrieve the latest meter data from Essent.""" - essent = PyEssent(self._username, self._password) - eans = set(essent.get_EANs()) - for possible_meter in eans: - meter_data = essent.read_meter(possible_meter, only_last_meter_reading=True) - if meter_data: - self._meter_data[possible_meter] = meter_data - - -class EssentMeter(SensorEntity): - """Representation of Essent measurements.""" - - def __init__(self, essent_base, meter, meter_type, tariff, unit): - """Initialize the sensor.""" - self._state = None - self._essent_base = essent_base - self._meter = meter - self._type = meter_type - self._tariff = tariff - self._unit = unit - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return f"{self._meter}-{self._type}-{self._tariff}" - - @property - def name(self): - """Return the name of the sensor.""" - return f"Essent {self._type} ({self._tariff})" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - if self._unit.lower() == "kwh": - return ENERGY_KILO_WATT_HOUR - - return self._unit - - def update(self): - """Fetch the energy usage.""" - # Ensure our data isn't too old - self._essent_base.update() - - # Retrieve our meter - data = self._essent_base.retrieve_meter_data(self._meter) - - # Set our value - self._state = next( - iter(data["values"]["LVR"][self._tariff]["records"].values()) - ) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index cec59742992..d7b1407642d 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -5,6 +5,7 @@ Such systems include evohome, Round Thermostat, and others. from __future__ import annotations from datetime import datetime as dt, timedelta +from http import HTTPStatus import logging import re from typing import Any @@ -19,8 +20,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, - HTTP_SERVICE_UNAVAILABLE, - HTTP_TOO_MANY_REQUESTS, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback @@ -158,13 +157,13 @@ def _handle_exception(err) -> bool: ) except aiohttp.ClientResponseError: - if err.status == HTTP_SERVICE_UNAVAILABLE: + if err.status == HTTPStatus.SERVICE_UNAVAILABLE: _LOGGER.warning( "The vendor says their server is currently unavailable. " "Check the vendor's service status page" ) - elif err.status == HTTP_TOO_MANY_REQUESTS: + elif err.status == HTTPStatus.TOO_MANY_REQUESTS: _LOGGER.warning( "The vendor's API rate limit has been exceeded. " "If this message persists, consider increasing the %s", diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 6dc2809630d..c293034e05a 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -230,9 +230,8 @@ class EvoZone(EvoChild, EvoClimateEntity): async def async_set_temperature(self, **kwargs) -> None: """Set a new target temperature.""" temperature = kwargs["temperature"] - until = kwargs.get("until") - if until is None: + if (until := kwargs.get("until")) is None: if self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW: await self._update_schedule() until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", "")) diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 495df9e697e..3d799a64e4d 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -59,11 +59,6 @@ class EvoDHW(EvoChild, WaterHeaterEntity): self._precision = PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE self._supported_features = SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE - @property - def state(self): - """Return the current state.""" - return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] - @property def current_operation(self) -> str: """Return the current operating mode (Auto, On, or Off).""" diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py index bc343f06065..e7d8be80509 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -1,19 +1,36 @@ """Support for Ezviz binary sensors.""" -import logging +from __future__ import annotations -from pyezviz.constants import BinarySensorType - -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + DEVICE_CLASS_UPDATE, + BinarySensorEntity, + BinarySensorEntityDescription, +) 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.update_coordinator import CoordinatorEntity -from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER +from .const import DATA_COORDINATOR, DOMAIN from .coordinator import EzvizDataUpdateCoordinator +from .entity import EzvizEntity -_LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 1 + +BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { + "Motion_Trigger": BinarySensorEntityDescription( + key="Motion_Trigger", + device_class=DEVICE_CLASS_MOTION, + ), + "alarm_schedules_enabled": BinarySensorEntityDescription( + key="alarm_schedules_enabled" + ), + "encrypted": BinarySensorEntityDescription(key="encrypted"), + "upgrade_available": BinarySensorEntityDescription( + key="upgrade_available", + device_class=DEVICE_CLASS_UPDATE, + ), +} async def async_setup_entry( @@ -23,24 +40,19 @@ async def async_setup_entry( coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] - sensors = [] - for idx, camera in enumerate(coordinator.data): - for name in camera: - # Only add sensor with value. - if camera.get(name) is None: - continue - - if name in BinarySensorType.__members__: - sensor_type_name = getattr(BinarySensorType, name).value - sensors.append( - EzvizBinarySensor(coordinator, idx, name, sensor_type_name) - ) - - async_add_entities(sensors) + async_add_entities( + [ + EzvizBinarySensor(coordinator, camera, binary_sensor) + for camera in coordinator.data + for binary_sensor, value in coordinator.data[camera].items() + if binary_sensor in BINARY_SENSOR_TYPES + if value is not None + ] + ) -class EzvizBinarySensor(CoordinatorEntity, BinarySensorEntity): +class EzvizBinarySensor(EzvizEntity, BinarySensorEntity): """Representation of a Ezviz sensor.""" coordinator: EzvizDataUpdateCoordinator @@ -48,46 +60,17 @@ class EzvizBinarySensor(CoordinatorEntity, BinarySensorEntity): def __init__( self, coordinator: EzvizDataUpdateCoordinator, - idx: int, - name: str, - sensor_type_name: str, + serial: str, + binary_sensor: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) - self._idx = idx - self._camera_name = self.coordinator.data[self._idx]["name"] - self._name = name - self._sensor_name = f"{self._camera_name}.{self._name}" - self.sensor_type_name = sensor_type_name - self._serial = self.coordinator.data[self._idx]["serial"] - - @property - def name(self) -> str: - """Return the name of the Ezviz sensor.""" - return self._name + super().__init__(coordinator, serial) + self._sensor_name = binary_sensor + self._attr_name = f"{self._camera_name} {binary_sensor.title()}" + self._attr_unique_id = f"{serial}_{self._camera_name}.{binary_sensor}" + self.entity_description = BINARY_SENSOR_TYPES[binary_sensor] @property def is_on(self) -> bool: """Return the state of the sensor.""" - return self.coordinator.data[self._idx][self._name] - - @property - def unique_id(self) -> str: - """Return the unique ID of this sensor.""" - return f"{self._serial}_{self._sensor_name}" - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._serial)}, - "name": self.coordinator.data[self._idx]["name"], - "model": self.coordinator.data[self._idx]["device_sub_category"], - "manufacturer": MANUFACTURER, - "sw_version": self.coordinator.data[self._idx]["version"], - } - - @property - def device_class(self) -> str: - """Device class for the sensor.""" - return self.sensor_type_name + return self.data[self._sensor_name] diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 44a90e2928f..89023b8902d 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -18,9 +18,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_DIRECTION, @@ -40,7 +38,6 @@ from .const import ( DIR_RIGHT, DIR_UP, DOMAIN, - MANUFACTURER, SERVICE_ALARM_SOUND, SERVICE_ALARM_TRIGER, SERVICE_DETECTION_SENSITIVITY, @@ -48,6 +45,7 @@ from .const import ( SERVICE_WAKE_DEVICE, ) from .coordinator import EzvizDataUpdateCoordinator +from .entity import EzvizEntity CAMERA_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} @@ -115,41 +113,37 @@ async def async_setup_entry( coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] - camera_config_entries = hass.config_entries.async_entries(DOMAIN) camera_entities = [] - for idx, camera in enumerate(coordinator.data): - - # There seem to be a bug related to localRtspPort in Ezviz API... - local_rtsp_port = DEFAULT_RTSP_PORT + for camera, value in coordinator.data.items(): camera_rtsp_entry = [ item - for item in camera_config_entries - if item.unique_id == camera[ATTR_SERIAL] + for item in hass.config_entries.async_entries(DOMAIN) + if item.unique_id == camera and item.source != SOURCE_IGNORE ] - if camera["local_rtsp_port"] != 0: - local_rtsp_port = camera["local_rtsp_port"] + # There seem to be a bug related to localRtspPort in Ezviz API. + local_rtsp_port = ( + value["local_rtsp_port"] + if value["local_rtsp_port"] != 0 + else DEFAULT_RTSP_PORT + ) if camera_rtsp_entry: - conf_cameras = camera_rtsp_entry[0] - # Skip ignored entities. - if conf_cameras.source == SOURCE_IGNORE: - continue + ffmpeg_arguments = camera_rtsp_entry[0].options[CONF_FFMPEG_ARGUMENTS] + camera_username = camera_rtsp_entry[0].data[CONF_USERNAME] + camera_password = camera_rtsp_entry[0].data[CONF_PASSWORD] - ffmpeg_arguments = conf_cameras.options.get( - CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS - ) - - camera_username = conf_cameras.data[CONF_USERNAME] - camera_password = conf_cameras.data[CONF_PASSWORD] - - camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{camera['local_ip']}:{local_rtsp_port}{ffmpeg_arguments}" + camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{value['local_ip']}:{local_rtsp_port}{ffmpeg_arguments}" _LOGGER.debug( - "Camera %s source stream: %s", camera[ATTR_SERIAL], camera_rtsp_stream + "Configuring Camera %s with ip: %s rtsp port: %s ffmpeg arguments: %s", + camera, + value["local_ip"], + local_rtsp_port, + ffmpeg_arguments, ) else: @@ -159,26 +153,27 @@ async def async_setup_entry( DOMAIN, context={"source": SOURCE_DISCOVERY}, data={ - ATTR_SERIAL: camera[ATTR_SERIAL], - CONF_IP_ADDRESS: camera["local_ip"], + ATTR_SERIAL: camera, + CONF_IP_ADDRESS: value["local_ip"], }, ) ) - camera_username = DEFAULT_CAMERA_USERNAME - camera_password = "" - camera_rtsp_stream = "" - ffmpeg_arguments = DEFAULT_FFMPEG_ARGUMENTS _LOGGER.warning( "Found camera with serial %s without configuration. Please go to integration to complete setup", - camera[ATTR_SERIAL], + camera, ) + ffmpeg_arguments = DEFAULT_FFMPEG_ARGUMENTS + camera_username = DEFAULT_CAMERA_USERNAME + camera_password = None + camera_rtsp_stream = "" + camera_entities.append( EzvizCamera( hass, coordinator, - idx, + camera, camera_username, camera_password, camera_rtsp_stream, @@ -230,7 +225,7 @@ async def async_setup_entry( ) -class EzvizCamera(CoordinatorEntity, Camera): +class EzvizCamera(EzvizEntity, Camera): """An implementation of a Ezviz security camera.""" coordinator: EzvizDataUpdateCoordinator @@ -239,69 +234,51 @@ class EzvizCamera(CoordinatorEntity, Camera): self, hass: HomeAssistant, coordinator: EzvizDataUpdateCoordinator, - idx: int, + serial: str, camera_username: str, - camera_password: str, + camera_password: str | None, camera_rtsp_stream: str | None, - local_rtsp_port: int | None, + local_rtsp_port: int, ffmpeg_arguments: str | None, ) -> None: """Initialize a Ezviz security camera.""" - super().__init__(coordinator) + super().__init__(coordinator, serial) Camera.__init__(self) self._username = camera_username self._password = camera_password self._rtsp_stream = camera_rtsp_stream - self._idx = idx - self._ffmpeg = hass.data[DATA_FFMPEG] self._local_rtsp_port = local_rtsp_port self._ffmpeg_arguments = ffmpeg_arguments - - self._serial = self.coordinator.data[self._idx]["serial"] - self._name = self.coordinator.data[self._idx]["name"] - self._local_ip = self.coordinator.data[self._idx]["local_ip"] + self._ffmpeg = hass.data[DATA_FFMPEG] + self._attr_unique_id = serial + self._attr_name = self.data["name"] @property def available(self) -> bool: """Return True if entity is available.""" - return self.coordinator.data[self._idx]["status"] != 2 + return self.data["status"] != 2 @property def supported_features(self) -> int: """Return supported features.""" - if self._rtsp_stream: + if self._password: return SUPPORT_STREAM return 0 - @property - def name(self) -> str: - """Return the name of this device.""" - return self._name - - @property - def model(self) -> str: - """Return the model of this device.""" - return self.coordinator.data[self._idx]["device_sub_category"] - - @property - def brand(self) -> str: - """Return the manufacturer of this device.""" - return MANUFACTURER - @property def is_on(self) -> bool: """Return true if on.""" - return bool(self.coordinator.data[self._idx]["status"]) + return bool(self.data["status"]) @property def is_recording(self) -> bool: """Return true if the device is recording.""" - return self.coordinator.data[self._idx]["alarm_notify"] + return self.data["alarm_notify"] @property def motion_detection_enabled(self) -> bool: """Camera Motion Detection Status.""" - return self.coordinator.data[self._idx]["alarm_notify"] + return self.data["alarm_notify"] def enable_motion_detection(self) -> None: """Enable motion detection in camera.""" @@ -319,11 +296,6 @@ class EzvizCamera(CoordinatorEntity, Camera): except InvalidHost as err: raise InvalidHost("Error disabling motion detection") from err - @property - def unique_id(self) -> str: - """Return the name of this camera.""" - return self._serial - async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: @@ -334,31 +306,24 @@ class EzvizCamera(CoordinatorEntity, Camera): self.hass, self._rtsp_stream, width=width, height=height ) - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._serial)}, - "name": self.coordinator.data[self._idx]["name"], - "model": self.coordinator.data[self._idx]["device_sub_category"], - "manufacturer": MANUFACTURER, - "sw_version": self.coordinator.data[self._idx]["version"], - } - async def stream_source(self) -> str | None: """Return the stream source.""" - local_ip = self.coordinator.data[self._idx]["local_ip"] - if self._local_rtsp_port: - rtsp_stream_source = ( - f"rtsp://{self._username}:{self._password}@" - f"{local_ip}:{self._local_rtsp_port}{self._ffmpeg_arguments}" - ) - _LOGGER.debug( - "Camera %s source stream: %s", self._serial, rtsp_stream_source - ) - self._rtsp_stream = rtsp_stream_source - return rtsp_stream_source - return None + if self._password is None: + return None + local_ip = self.data["local_ip"] + self._rtsp_stream = ( + f"rtsp://{self._username}:{self._password}@" + f"{local_ip}:{self._local_rtsp_port}{self._ffmpeg_arguments}" + ) + _LOGGER.debug( + "Configuring Camera %s with ip: %s rtsp port: %s ffmpeg arguments: %s", + self._serial, + local_ip, + self._local_rtsp_port, + self._ffmpeg_arguments, + ) + + return self._rtsp_stream def perform_ptz(self, direction: str, speed: int) -> None: """Perform a PTZ action on the camera.""" diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py new file mode 100644 index 00000000000..288c4a5d9eb --- /dev/null +++ b/homeassistant/components/ezviz/entity.py @@ -0,0 +1,36 @@ +"""An abstract class common to all Ezviz entities.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import EzvizDataUpdateCoordinator + + +class EzvizEntity(CoordinatorEntity, Entity): + """Generic entity encapsulating common features of Ezviz device.""" + + def __init__( + self, + coordinator: EzvizDataUpdateCoordinator, + serial: str, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._serial = serial + self._camera_name = self.data["name"] + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial)}, + manufacturer=MANUFACTURER, + model=self.data["device_sub_category"], + name=self.data["name"], + sw_version=self.data["version"], + ) + + @property + def data(self) -> dict[str, Any]: + """Return coordinator data for this entity.""" + return self.coordinator.data[self._serial] diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index d5a38b17755..1108f1a6f83 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.9"], + "requirements": ["pyezviz==0.1.9.4"], "config_flow": true, "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index 512491a2548..3ea650154f0 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -1,21 +1,42 @@ """Support for Ezviz sensors.""" from __future__ import annotations -import logging - -from pyezviz.constants import SensorType - -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE 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 .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER +from .const import DATA_COORDINATOR, DOMAIN from .coordinator import EzvizDataUpdateCoordinator +from .entity import EzvizEntity -_LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 1 + +SENSOR_TYPES: dict[str, SensorEntityDescription] = { + "sw_version": SensorEntityDescription(key="sw_version"), + "battery_level": SensorEntityDescription( + key="battery_level", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + ), + "alarm_sound_mod": SensorEntityDescription(key="alarm_sound_mod"), + "detection_sensibility": SensorEntityDescription(key="detection_sensibility"), + "last_alarm_time": SensorEntityDescription(key="last_alarm_time"), + "Seconds_Last_Trigger": SensorEntityDescription( + key="Seconds_Last_Trigger", + entity_registry_enabled_default=False, + ), + "last_alarm_pic": SensorEntityDescription(key="last_alarm_pic"), + "supported_channels": SensorEntityDescription(key="supported_channels"), + "local_ip": SensorEntityDescription(key="local_ip"), + "wan_ip": SensorEntityDescription(key="wan_ip"), + "PIR_Status": SensorEntityDescription( + key="PIR_Status", + device_class=DEVICE_CLASS_MOTION, + ), +} async def async_setup_entry( @@ -25,69 +46,34 @@ async def async_setup_entry( coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] - sensors = [] - for idx, camera in enumerate(coordinator.data): - for name in camera: - # Only add sensor with value. - if camera.get(name) is None: - continue - - if name in SensorType.__members__: - sensor_type_name = getattr(SensorType, name).value - sensors.append(EzvizSensor(coordinator, idx, name, sensor_type_name)) - - async_add_entities(sensors) + async_add_entities( + [ + EzvizSensor(coordinator, camera, sensor) + for camera in coordinator.data + for sensor, value in coordinator.data[camera].items() + if sensor in SENSOR_TYPES + if value is not None + ] + ) -class EzvizSensor(CoordinatorEntity, SensorEntity): +class EzvizSensor(EzvizEntity, SensorEntity): """Representation of a Ezviz sensor.""" coordinator: EzvizDataUpdateCoordinator def __init__( - self, - coordinator: EzvizDataUpdateCoordinator, - idx: int, - name: str, - sensor_type_name: str, + self, coordinator: EzvizDataUpdateCoordinator, serial: str, sensor: str ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) - self._idx = idx - self._camera_name = self.coordinator.data[self._idx]["name"] - self._name = name - self._sensor_name = f"{self._camera_name}.{self._name}" - self.sensor_type_name = sensor_type_name - self._serial = self.coordinator.data[self._idx]["serial"] - - @property - def name(self) -> str: - """Return the name of the Ezviz sensor.""" - return self._name + super().__init__(coordinator, serial) + self._sensor_name = sensor + self._attr_name = f"{self._camera_name} {sensor.title()}" + self._attr_unique_id = f"{serial}_{self._camera_name}.{sensor}" + self.entity_description = SENSOR_TYPES[sensor] @property def native_value(self) -> int | str: """Return the state of the sensor.""" - return self.coordinator.data[self._idx][self._name] - - @property - def unique_id(self) -> str: - """Return the unique ID of this sensor.""" - return f"{self._serial}_{self._sensor_name}" - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._serial)}, - "name": self.coordinator.data[self._idx]["name"], - "model": self.coordinator.data[self._idx]["device_sub_category"], - "manufacturer": MANUFACTURER, - "sw_version": self.coordinator.data[self._idx]["version"], - } - - @property - def device_class(self) -> str: - """Device class for the sensor.""" - return self.sensor_type_name + return self.data[self._sensor_name] diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py index 9949dc18b23..0324d508f7f 100644 --- a/homeassistant/components/ezviz/switch.py +++ b/homeassistant/components/ezviz/switch.py @@ -1,7 +1,6 @@ """Support for Ezviz Switch sensors.""" from __future__ import annotations -import logging from typing import Any from pyezviz.constants import DeviceSwitchType @@ -10,14 +9,11 @@ from pyezviz.exceptions import HTTPError, PyEzvizError from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity 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.update_coordinator import CoordinatorEntity -from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER +from .const import DATA_COORDINATOR, DOMAIN from .coordinator import EzvizDataUpdateCoordinator - -_LOGGER = logging.getLogger(__name__) +from .entity import EzvizEntity async def async_setup_entry( @@ -27,51 +23,40 @@ async def async_setup_entry( coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] - switch_entities = [] + supported_switches = {switches.value for switches in DeviceSwitchType} - for idx, camera in enumerate(coordinator.data): - if not camera.get("switches"): - continue - for switch in camera["switches"]: - if switch not in supported_switches: - continue - switch_entities.append(EzvizSwitch(coordinator, idx, switch)) - - async_add_entities(switch_entities) + async_add_entities( + [ + EzvizSwitch(coordinator, camera, switch) + for camera in coordinator.data + for switch in coordinator.data[camera].get("switches") + if switch in supported_switches + ] + ) -class EzvizSwitch(CoordinatorEntity, SwitchEntity): +class EzvizSwitch(EzvizEntity, SwitchEntity): """Representation of a Ezviz sensor.""" coordinator: EzvizDataUpdateCoordinator + ATTR_DEVICE_CLASS = DEVICE_CLASS_SWITCH def __init__( - self, coordinator: EzvizDataUpdateCoordinator, idx: int, switch: str + self, coordinator: EzvizDataUpdateCoordinator, serial: str, switch: str ) -> None: """Initialize the switch.""" - super().__init__(coordinator) - self._idx = idx - self._camera_name = self.coordinator.data[self._idx]["name"] + super().__init__(coordinator, serial) self._name = switch - self._sensor_name = f"{self._camera_name}.{DeviceSwitchType(self._name).name}" - self._serial = self.coordinator.data[self._idx]["serial"] - self._device_class = DEVICE_CLASS_SWITCH - - @property - def name(self) -> str: - """Return the name of the Ezviz switch.""" - return f"{DeviceSwitchType(self._name).name}" + self._attr_name = f"{self._camera_name} {DeviceSwitchType(switch).name.title()}" + self._attr_unique_id = ( + f"{serial}_{self._camera_name}.{DeviceSwitchType(switch).name}" + ) @property def is_on(self) -> bool: """Return the state of the switch.""" - return self.coordinator.data[self._idx]["switches"][self._name] - - @property - def unique_id(self) -> str: - """Return the unique ID of this switch.""" - return f"{self._serial}_{self._sensor_name}" + return self.data["switches"][self._name] async def async_turn_on(self, **kwargs: Any) -> None: """Change a device switch on the camera.""" @@ -98,19 +83,3 @@ class EzvizSwitch(CoordinatorEntity, SwitchEntity): if update_ok: await self.coordinator.async_request_refresh() - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._serial)}, - "name": self.coordinator.data[self._idx]["name"], - "model": self.coordinator.data[self._idx]["device_sub_category"], - "manufacturer": MANUFACTURER, - "sw_version": self.coordinator.data[self._idx]["version"], - } - - @property - def device_class(self) -> str: - """Device class for the sensor.""" - return self._device_class diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index c270a878d49..e27916ec6c1 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/faa_delays/translations/bg.json b/homeassistant/components/faa_delays/translations/bg.json index 0995436221b..93fa3f04d6c 100644 --- a/homeassistant/components/faa_delays/translations/bg.json +++ b/homeassistant/components/faa_delays/translations/bg.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u0422\u043e\u0432\u0430 \u043b\u0435\u0442\u0438\u0449\u0435 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e." + }, "error": { + "invalid_airport": "\u041a\u043e\u0434\u044a\u0442 \u043d\u0430 \u043b\u0435\u0442\u0438\u0449\u0435\u0442\u043e \u043d\u0435 \u0435 \u0432\u0430\u043b\u0438\u0434\u0435\u043d", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/facebook/notify.py b/homeassistant/components/facebook/notify.py index 0ce2fbfc665..ea8848a5af2 100644 --- a/homeassistant/components/facebook/notify.py +++ b/homeassistant/components/facebook/notify.py @@ -1,4 +1,5 @@ """Facebook platform for notify component.""" +from http import HTTPStatus import json import logging @@ -12,7 +13,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK +from homeassistant.const import CONTENT_TYPE_JSON import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -76,7 +77,7 @@ class FacebookNotificationService(BaseNotificationService): headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, timeout=10, ) - if resp.status_code != HTTP_OK: + if resp.status_code != HTTPStatus.OK: log_error(resp) diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index 5c90ce73560..ba95d1cd476 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -1,5 +1,6 @@ """Component for facial detection and identification via facebox.""" import base64 +from http import HTTPStatus import logging import requests @@ -21,9 +22,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - HTTP_BAD_REQUEST, - HTTP_OK, - HTTP_UNAUTHORIZED, ) from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv @@ -67,10 +65,10 @@ def check_box_health(url, username, password): kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password) try: response = requests.get(url, **kwargs) - if response.status_code == HTTP_UNAUTHORIZED: + if response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) return None - if response.status_code == HTTP_OK: + if response.status_code == HTTPStatus.OK: return response.json()["hostname"] except requests.exceptions.ConnectionError: _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) @@ -115,7 +113,7 @@ def post_image(url, image, username, password): kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password) try: response = requests.post(url, json={"base64": encode_image(image)}, **kwargs) - if response.status_code == HTTP_UNAUTHORIZED: + if response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) return None return response @@ -137,9 +135,9 @@ def teach_file(url, name, file_path, username, password): files={"file": open_file}, **kwargs, ) - if response.status_code == HTTP_UNAUTHORIZED: + if response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) - elif response.status_code == HTTP_BAD_REQUEST: + elif response.status_code == HTTPStatus.BAD_REQUEST: _LOGGER.error( "%s teaching of file %s failed with message:%s", CLASSIFIER, diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index a05505e8112..3a5b9bcda67 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -458,13 +458,10 @@ class FanEntity(ToggleEntity): @property def speed(self) -> str | None: """Return the current speed.""" - if self._implemented_preset_mode: - preset_mode = self.preset_mode - if preset_mode: - return preset_mode + if self._implemented_preset_mode and (preset_mode := self.preset_mode): + return preset_mode if self._implemented_percentage: - percentage = self.percentage - if percentage is None: + if (percentage := self.percentage) is None: return None return self.percentage_to_speed(percentage) return None diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index 2d4244ec2dc..c18e8352b24 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -50,9 +50,7 @@ async def _async_reproduce_state( 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: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/fan/translations/ca.json b/homeassistant/components/fan/translations/ca.json index 7c1789aeb24..da5296b34f0 100644 --- a/homeassistant/components/fan/translations/ca.json +++ b/homeassistant/components/fan/translations/ca.json @@ -15,8 +15,8 @@ }, "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Ventilador" diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index fa1f18815f1..2a82cee7cea 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -56,8 +56,7 @@ class SpeedtestSensor(RestoreEntity, SensorEntity): def update(self) -> None: """Get the latest data and update the states.""" - data = self._speedtest_data.data # type: ignore[attr-defined] - if data is None: + if (data := self._speedtest_data.data) is None: # type: ignore[attr-defined] return self._attr_native_value = data["download"] diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 964c1112840..cff4e153d98 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -307,8 +307,7 @@ class FibaroController: device.device_config = self._device_config.get(device.ha_id, {}) else: device.mapped_type = None - dtype = device.mapped_type - if dtype is None: + if (dtype := device.mapped_type) is None: continue device.unique_id_str = f"{self.hub_serial}.{device.id}" self._device_map[device.id] = device @@ -472,12 +471,11 @@ class FibaroDevice(Entity): @property def current_power_w(self): """Return the current power usage in W.""" - if "power" in self.fibaro_device.properties: - power = self.fibaro_device.properties.power - if power: - return convert(power, float, 0.0) - else: - return None + if "power" in self.fibaro_device.properties and ( + power := self.fibaro_device.properties.power + ): + return convert(power, float, 0.0) + return None @property def current_binary_state(self): diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index aed1da543ee..fa9248dc11e 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -60,10 +60,25 @@ class FibaroLight(FibaroDevice, LightEntity): devconf = fibaro_device.device_config self._reset_color = devconf.get(CONF_RESET_COLOR, False) supports_color = ( - "color" in fibaro_device.properties and "setColor" in fibaro_device.actions + "color" in fibaro_device.properties + or "colorComponents" in fibaro_device.properties + or "RGB" in fibaro_device.type + or "rgb" in fibaro_device.type + or "color" in fibaro_device.baseType + ) and ( + "setColor" in fibaro_device.actions + or "setColorComponents" in fibaro_device.actions + ) + supports_white_v = ( + "setW" in fibaro_device.actions + or "RGBW" in fibaro_device.type + or "rgbw" in fibaro_device.type + ) + supports_dimming = ( + "levelChange" in fibaro_device.interfaces + or supports_color + or supports_white_v ) - supports_dimming = "levelChange" in fibaro_device.interfaces - supports_white_v = "setW" in fibaro_device.actions # Configuration can override default capability detection if devconf.get(CONF_DIMMING, supports_dimming): diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 6964abd9b3d..0a06c7a2b07 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -218,8 +218,7 @@ class FidoSensor(SensorEntity): async def async_update(self): """Get the latest data from Fido and update the state.""" await self.fido_data.async_update() - sensor_type = self.entity_description.key - if sensor_type == "balance": + if (sensor_type := self.entity_description.key) == "balance": if self.fido_data.data.get(sensor_type) is not None: self._attr_native_value = round(self.fido_data.data[sensor_type], 2) else: diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index 73b262c9090..6a18fa4cc8b 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -34,9 +34,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= file_path = config.get(CONF_FILE_PATH) name = config.get(CONF_NAME) unit = config.get(CONF_UNIT_OF_MEASUREMENT) - value_template = config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: + if (value_template := config.get(CONF_VALUE_TEMPLATE)) is not None: value_template.hass = hass if hass.config.is_allowed_path(file_path): diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index d584bbed4bb..a7bec82cee0 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -79,8 +79,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.info("Skipping account %s for bank %s", account.iban, fints_name) continue - account_name = account_config.get(account.iban) - if not account_name: + if not (account_name := account_config.get(account.iban)): account_name = f"{fints_name} - {account.iban}" accounts.append(FinTsAccount(client, account, account_name)) _LOGGER.debug("Creating account %s for bank %s", account.iban, fints_name) diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index ec446621212..a87b1609ec9 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -67,9 +67,8 @@ class IncidentsSensor(RestoreEntity, SensorEntity): def extra_state_attributes(self) -> object: """Return available attributes for sensor.""" attr = {} - data = self._state_attributes - if not data: + if not (data := self._state_attributes): return attr for value in ( diff --git a/homeassistant/components/fireservicerota/translations/bg.json b/homeassistant/components/fireservicerota/translations/bg.json new file mode 100644 index 00000000000..22cc783d4e9 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/bg.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d" + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "url": "\u0423\u0435\u0431\u0441\u0430\u0439\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/hu.json b/homeassistant/components/fireservicerota/translations/hu.json index 54461091c93..3bda2225400 100644 --- a/homeassistant/components/fireservicerota/translations/hu.json +++ b/homeassistant/components/fireservicerota/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" diff --git a/homeassistant/components/firmata/entity.py b/homeassistant/components/firmata/entity.py index e9f9f3619fe..0f248e0b9d7 100644 --- a/homeassistant/components/firmata/entity.py +++ b/homeassistant/components/firmata/entity.py @@ -19,13 +19,13 @@ class FirmataEntity: @property def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "connections": {}, - "identifiers": {(DOMAIN, self._api.board.name)}, - "manufacturer": FIRMATA_MANUFACTURER, - "name": self._api.board.name, - "sw_version": self._api.board.firmware_version, - } + return DeviceInfo( + connections={}, + identifiers={(DOMAIN, self._api.board.name)}, + manufacturer=FIRMATA_MANUFACTURER, + name=self._api.board.name, + sw_version=self._api.board.firmware_version, + ) class FirmataPinEntity(FirmataEntity): diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 34c4f61f554..2de121920af 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -29,7 +29,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.helpers.network import get_url +from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.json import load_json, save_json @@ -101,16 +101,21 @@ def request_app_setup( else: setup_platform(hass, config, add_entities, discovery_info) - start_url = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" - - description = f"""Please create a Fitbit developer app at + try: + description = f"""Please create a Fitbit developer app at https://dev.fitbit.com/apps/new. For the OAuth 2.0 Application Type choose Personal. - Set the Callback URL to {start_url}. + Set the Callback URL to {get_url(hass, require_ssl=True)}{FITBIT_AUTH_CALLBACK_PATH}. + (Note: Your Home Assistant instance must be accessible via HTTPS.) They will provide you a Client ID and secret. These need to be saved into the file located at: {config_path}. Then come back here and hit the below button. """ + except NoURLAvailableError: + error_msg = """Could not find a SSL enabled URL for your Home Assistant instance. + Fitbit requires that your Home Assistant instance is accessible via HTTPS. + """ + configurator.notify_errors(_CONFIGURING["fitbit"], error_msg) submit = "I have saved my Client ID and Client Secret into fitbit.conf." @@ -136,7 +141,7 @@ def request_oauth_completion(hass: HomeAssistant) -> None: def fitbit_configuration_callback(fields: list[dict[str, str]]) -> None: """Handle configuration updates.""" - start_url = f"{get_url(hass)}{FITBIT_AUTH_START}" + start_url = f"{get_url(hass, require_ssl=True)}{FITBIT_AUTH_START}" description = f"Please authorize Fitbit by visiting {start_url}" @@ -236,7 +241,7 @@ def setup_platform( config_file.get(CONF_CLIENT_ID), config_file.get(CONF_CLIENT_SECRET) ) - redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" + redirect_uri = f"{get_url(hass, require_ssl=True)}{FITBIT_AUTH_CALLBACK_PATH}" fitbit_auth_start_url, _ = oauth.authorize_token_url( redirect_uri=redirect_uri, @@ -372,12 +377,13 @@ class FitbitSensor(SensorEntity): @property def icon(self) -> str | None: """Icon to use in the frontend, if any.""" - if self.entity_description.key == "devices/battery" and self.extra is not None: - extra_battery = self.extra.get("battery") - if extra_battery is not None: - battery_level = BATTERY_LEVELS.get(extra_battery) - if battery_level is not None: - return icon_for_battery_level(battery_level=battery_level) + if ( + self.entity_description.key == "devices/battery" + and self.extra is not None + and (extra_battery := self.extra.get("battery")) is not None + and (battery_level := BATTERY_LEVELS.get(extra_battery)) is not None + ): + return icon_for_battery_level(battery_level=battery_level) return self.entity_description.icon @property diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index ac22e788a6e..f5cedad243d 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -9,7 +9,7 @@ import logging from bleak import BleakScanner from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from fjaraskupan import Device, State, device_filter +from fjaraskupan import UUID_SERVICE, Device, State, device_filter from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -23,7 +23,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DISPATCH_DETECTION, DOMAIN -PLATFORMS = ["binary_sensor", "fan", "light", "sensor"] +PLATFORMS = ["binary_sensor", "fan", "light", "sensor", "number"] _LOGGER = logging.getLogger(__name__) @@ -48,7 +48,7 @@ class EntryState: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner() + scanner = BleakScanner(filters={"UUIDs": [str(UUID_SERVICE)]}) state = EntryState(scanner, {}) hass.data.setdefault(DOMAIN, {}) @@ -57,19 +57,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def detection_callback( ble_device: BLEDevice, advertisement_data: AdvertisementData ) -> None: - if not device_filter(ble_device, advertisement_data): - return + if data := state.devices.get(ble_device.address): + _LOGGER.debug( + "Update: %s %s - %s", ble_device.name, ble_device, advertisement_data + ) - _LOGGER.debug( - "Detection: %s %s - %s", ble_device.name, ble_device, advertisement_data - ) - - data = state.devices.get(ble_device.address) - - if data: data.device.detection_callback(ble_device, advertisement_data) data.coordinator.async_set_updated_data(data.device.state) else: + if not device_filter(ble_device, advertisement_data): + return + + _LOGGER.debug( + "Detected: %s %s - %s", ble_device.name, ble_device, advertisement_data + ) device = Device(ble_device) device.detection_callback(ble_device, advertisement_data) @@ -88,11 +89,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator.async_set_updated_data(device.state) - device_info: DeviceInfo = { - "identifiers": {(DOMAIN, ble_device.address)}, - "manufacturer": "Fjäråskupan", - "name": "Fjäråskupan", - } + device_info = DeviceInfo( + identifiers={(DOMAIN, ble_device.address)}, + manufacturer="Fjäråskupan", + name="Fjäråskupan", + ) device_state = DeviceState(device, coordinator, device_info) state.devices[ble_device.address] = device_state async_dispatcher_send( diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index 9b82ae1199b..4d4d1882dcd 100644 --- a/homeassistant/components/fjaraskupan/config_flow.py +++ b/homeassistant/components/fjaraskupan/config_flow.py @@ -7,7 +7,7 @@ import async_timeout from bleak import BleakScanner from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from fjaraskupan import device_filter +from fjaraskupan import UUID_SERVICE, device_filter from homeassistant.helpers.config_entry_flow import register_discovery_flow @@ -25,7 +25,9 @@ async def _async_has_devices(hass) -> bool: if device_filter(device, advertisement_data): event.set() - async with BleakScanner(detection_callback=detection): + async with BleakScanner( + detection_callback=detection, filters={"UUIDs": [str(UUID_SERVICE)]} + ): try: async with async_timeout.timeout(CONST_WAIT_TIME): await event.wait() diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index 4a81e70b848..7cb7c7cd18e 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -35,10 +35,12 @@ ORDERED_NAMED_FAN_SPEEDS = ["1", "2", "3", "4", "5", "6", "7", "8"] PRESET_MODE_NORMAL = "normal" PRESET_MODE_AFTER_COOKING_MANUAL = "after_cooking_manual" PRESET_MODE_AFTER_COOKING_AUTO = "after_cooking_auto" +PRESET_MODE_PERIODIC_VENTILATION = "periodic_ventilation" PRESET_MODES = [ PRESET_MODE_NORMAL, PRESET_MODE_AFTER_COOKING_AUTO, PRESET_MODE_AFTER_COOKING_MANUAL, + PRESET_MODE_PERIODIC_VENTILATION, ] PRESET_TO_COMMAND = { @@ -48,6 +50,10 @@ PRESET_TO_COMMAND = { } +class UnsupportedPreset(Exception): + """The preset is unsupported.""" + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -112,7 +118,10 @@ class Fan(CoordinatorEntity[State], FanEntity): async with self._device: if preset_mode != self._preset_mode: - await self._device.send_command(PRESET_TO_COMMAND[preset_mode]) + if command := PRESET_TO_COMMAND.get(preset_mode): + await self._device.send_command(command) + else: + raise UnsupportedPreset(f"The preset {preset_mode} is unsupported") if preset_mode == PRESET_MODE_NORMAL: await self._device.send_fan_speed(int(new_speed)) @@ -125,8 +134,11 @@ class Fan(CoordinatorEntity[State], FanEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - await self._device.send_command(PRESET_TO_COMMAND[preset_mode]) - self.coordinator.async_set_updated_data(self._device.state) + if command := PRESET_TO_COMMAND.get(preset_mode): + await self._device.send_command(command) + self.coordinator.async_set_updated_data(self._device.state) + else: + raise UnsupportedPreset(f"The preset {preset_mode} is unsupported") async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" @@ -181,6 +193,8 @@ class Fan(CoordinatorEntity[State], FanEntity): self._preset_mode = PRESET_MODE_AFTER_COOKING_MANUAL else: self._preset_mode = PRESET_MODE_AFTER_COOKING_AUTO + elif data.periodic_venting_on: + self._preset_mode = PRESET_MODE_PERIODIC_VENTILATION else: self._preset_mode = PRESET_MODE_NORMAL diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index d9cd5640848..fb27d8b803f 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fjaraskupan", "requirements": [ - "fjaraskupan==1.0.1" + "fjaraskupan==1.0.2" ], "codeowners": [ "@elupus" diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py new file mode 100644 index 00000000000..d5862bf2e7f --- /dev/null +++ b/homeassistant/components/fjaraskupan/number.py @@ -0,0 +1,70 @@ +"""Support for sensors.""" +from __future__ import annotations + +from fjaraskupan import Device, State + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG, TIME_MINUTES +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import DeviceState, async_setup_entry_platform + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors dynamically through discovery.""" + + def _constructor(device_state: DeviceState) -> list[Entity]: + return [ + PeriodicVentingTime( + device_state.coordinator, device_state.device, device_state.device_info + ), + ] + + async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) + + +class PeriodicVentingTime(CoordinatorEntity[State], NumberEntity): + """Periodic Venting.""" + + _attr_max_value: float = 59 + _attr_min_value: float = 0 + _attr_step: float = 1 + _attr_entity_registry_enabled_default = True + _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_unit_of_measurement = TIME_MINUTES + + def __init__( + self, + coordinator: DataUpdateCoordinator[State], + device: Device, + device_info: DeviceInfo, + ) -> None: + """Init sensor.""" + super().__init__(coordinator) + self._device = device + self._attr_unique_id = f"{device.address}-periodic-venting" + self._attr_device_info = device_info + self._attr_name = f"{device_info['name']} Periodic Venting" + + @property + def value(self) -> float | None: + """Return the entity value to represent the entity state.""" + if data := self.coordinator.data: + return data.periodic_venting + return None + + async def async_set_value(self, value: float) -> None: + """Set new value.""" + await self._device.send_periodic_venting(int(value)) + self.coordinator.async_set_updated_data(self._device.state) diff --git a/homeassistant/components/fjaraskupan/sensor.py b/homeassistant/components/fjaraskupan/sensor.py index 4252828c633..1821008a1d7 100644 --- a/homeassistant/components/fjaraskupan/sensor.py +++ b/homeassistant/components/fjaraskupan/sensor.py @@ -9,7 +9,10 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -57,6 +60,7 @@ class RssiSensor(CoordinatorEntity[State], SensorEntity): self._attr_state_class = STATE_CLASS_MEASUREMENT self._attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT self._attr_entity_registry_enabled_default = False + self._attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @property def native_value(self) -> StateType: diff --git a/homeassistant/components/fjaraskupan/translations/bg.json b/homeassistant/components/fjaraskupan/translations/bg.json new file mode 100644 index 00000000000..4db9b1af40e --- /dev/null +++ b/homeassistant/components/fjaraskupan/translations/bg.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/translations/cs.json b/homeassistant/components/fjaraskupan/translations/cs.json index 3f0012e00d2..5f890becd56 100644 --- a/homeassistant/components/fjaraskupan/translations/cs.json +++ b/homeassistant/components/fjaraskupan/translations/cs.json @@ -1,7 +1,13 @@ { "config": { "abort": { - "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "step": { + "confirm": { + "description": "Chcete nastavit Fj\u00e4r\u00e5skupan?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index 690cbe03cdd..ca634310659 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/flick_electric/translations/bg.json b/homeassistant/components/flick_electric/translations/bg.json new file mode 100644 index 00000000000..5e1ec63739a --- /dev/null +++ b/homeassistant/components/flick_electric/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "client_id": "Client ID (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)", + "client_secret": "Client Secret (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index fd7c3f5c02a..9280c77f95c 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -7,7 +7,7 @@ from flipr_api import FliprAPIRestClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -88,10 +88,10 @@ class FliprEntity(CoordinatorEntity): flipr_id = coordinator.config_entry.data[CONF_FLIPR_ID] self._attr_unique_id = f"{flipr_id}-{description.key}" - self._attr_device_info = { - "identifiers": {(DOMAIN, flipr_id)}, - "name": NAME, - "manufacturer": MANUFACTURER, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, flipr_id)}, + manufacturer=MANUFACTURER, + name=NAME, + ) self._attr_name = f"Flipr {flipr_id} {description.name}" diff --git a/homeassistant/components/flipr/translations/bg.json b/homeassistant/components/flipr/translations/bg.json new file mode 100644 index 00000000000..51ee3653e15 --- /dev/null +++ b/homeassistant/components/flipr/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 734c4d9e766..32802ba85d3 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index 9f0e8029888..280f19dc57e 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -33,14 +33,14 @@ class FloEntity(Entity): @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return { - "identifiers": {(FLO_DOMAIN, self._device.id)}, - "connections": {(CONNECTION_NETWORK_MAC, self._device.mac_address)}, - "manufacturer": self._device.manufacturer, - "model": self._device.model, - "name": self._device.device_name, - "sw_version": self._device.firmware_version, - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._device.mac_address)}, + identifiers={(FLO_DOMAIN, self._device.id)}, + manufacturer=self._device.manufacturer, + model=self._device.model, + name=self._device.device_name, + sw_version=self._device.firmware_version, + ) @property def available(self) -> bool: diff --git a/homeassistant/components/flo/translations/bg.json b/homeassistant/components/flo/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/flo/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index 7bdd1b33c5b..de5c078f714 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -1,12 +1,13 @@ """Flock platform for notify component.""" import asyncio +from http import HTTPStatus import logging import async_timeout import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService -from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_OK +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -44,7 +45,7 @@ class FlockNotificationService(BaseNotificationService): response = await self._session.post(self._url, json=payload) result = await response.json() - if response.status != HTTP_OK or "error" in result: + if response.status != HTTPStatus.OK or "error" in result: _LOGGER.error( "Flock service returned HTTP status %d, response %s", response.status, diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index 1b441aa6ba5..3ca99a335f2 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -71,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index ff4610ca788..2ff7712cfd5 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -132,12 +133,12 @@ class FlumeSensor(CoordinatorEntity, SensorEntity): self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{description.key}_{device_id}" - self._attr_device_info = { - "name": self.name, - "identifiers": {(DOMAIN, device_id)}, - "manufacturer": "Flume, Inc.", - "model": "Flume Smart Water Monitor", - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + manufacturer="Flume, Inc.", + model="Flume Smart Water Monitor", + name=self.name, + ) @property def native_value(self): diff --git a/homeassistant/components/flume/translations/bg.json b/homeassistant/components/flume/translations/bg.json new file mode 100644 index 00000000000..6eca91e8ed2 --- /dev/null +++ b/homeassistant/components/flume/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/hu.json b/homeassistant/components/flume/translations/hu.json index e1780be5654..d1cab31095d 100644 --- a/homeassistant/components/flume/translations/hu.json +++ b/homeassistant/components/flume/translations/hu.json @@ -14,7 +14,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "A(z) {username} jelszava m\u00e1r nem \u00e9rv\u00e9nyes.", + "description": "{username} jelszava m\u00e1r nem \u00e9rv\u00e9nyes.", "title": "Hiteles\u00edtse \u00fajra Flume-fi\u00f3kj\u00e1t" }, "user": { diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 22de54180a6..86a86e440c9 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -15,13 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ( - CATEGORY_CDC_REPORT, - CATEGORY_USER_REPORT, - DATA_COORDINATOR, - DOMAIN, - LOGGER, -) +from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DOMAIN, LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(minutes=30) @@ -32,8 +26,7 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flu Near You as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}}) - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} + hass.data.setdefault(DOMAIN, {}) websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) @@ -57,11 +50,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return data + coordinators = {} data_init_tasks = [] + for api_category in (CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT): - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - api_category - ] = DataUpdateCoordinator( + coordinator = coordinators[api_category] = DataUpdateCoordinator( hass, LOGGER, name=f"{api_category} ({latitude}, {longitude})", @@ -71,6 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data_init_tasks.append(coordinator.async_refresh()) await asyncio.gather(*data_init_tasks) + hass.data[DOMAIN][entry.entry_id] = coordinators hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -81,6 +75,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an Flu Near You config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/flunearyou/const.py b/homeassistant/components/flunearyou/const.py index 96df29aa300..dc9ac629d92 100644 --- a/homeassistant/components/flunearyou/const.py +++ b/homeassistant/components/flunearyou/const.py @@ -4,7 +4,5 @@ import logging DOMAIN = "flunearyou" LOGGER = logging.getLogger(__package__) -DATA_COORDINATOR = "coordinator" - CATEGORY_CDC_REPORT = "cdc_report" CATEGORY_USER_REPORT = "user_report" diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 1a7aba5966b..a30c2423253 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -24,7 +24,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DATA_COORDINATOR, DOMAIN +from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DOMAIN ATTR_CITY = "city" ATTR_REPORTED_DATE = "reported_date" @@ -122,7 +122,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Flu Near You sensors based on a config entry.""" - coordinators = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] + coordinators = hass.data[DOMAIN][entry.entry_id] sensors: list[CdcSensor | UserSensor] = [ CdcSensor(coordinators[CATEGORY_CDC_REPORT], entry, description) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 572d6e3c983..248ce7261e9 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -1 +1,198 @@ -"""The flux_led component.""" +"""The Flux LED/MagicLight integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any, Final + +from flux_led import DeviceType +from flux_led.aio import AIOWifiLedBulb +from flux_led.aioscanner import AIOBulbScanner + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + DISCOVER_SCAN_TIMEOUT, + DOMAIN, + FLUX_HOST, + FLUX_LED_DISCOVERY, + FLUX_LED_EXCEPTIONS, + FLUX_MAC, + FLUX_MODEL, + SIGNAL_STATE_UPDATED, + STARTUP_SCAN_TIMEOUT, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS_BY_TYPE: Final = {DeviceType.Bulb: ["light"], DeviceType.Switch: ["switch"]} +DISCOVERY_INTERVAL: Final = timedelta(minutes=15) +REQUEST_REFRESH_DELAY: Final = 1.5 + + +@callback +def async_wifi_bulb_for_host(host: str) -> AIOWifiLedBulb: + """Create a AIOWifiLedBulb from a host.""" + return AIOWifiLedBulb(host) + + +@callback +def async_update_entry_from_discovery( + hass: HomeAssistant, entry: config_entries.ConfigEntry, device: dict[str, Any] +) -> None: + """Update a config entry from a flux_led discovery.""" + name = f"{device[FLUX_MODEL]} {device[FLUX_MAC]}" + hass.config_entries.async_update_entry( + entry, + data={**entry.data, CONF_NAME: name}, + title=name, + unique_id=dr.format_mac(device[FLUX_MAC]), + ) + + +async def async_discover_devices( + hass: HomeAssistant, timeout: int, address: str | None = None +) -> list[dict[str, str]]: + """Discover flux led devices.""" + scanner = AIOBulbScanner() + try: + discovered: list[dict[str, str]] = await scanner.async_scan( + timeout=timeout, address=address + ) + except OSError as ex: + _LOGGER.debug("Scanning failed with error: %s", ex) + return [] + else: + return discovered + + +async def async_discover_device( + hass: HomeAssistant, host: str +) -> dict[str, str] | None: + """Direct discovery at a single ip instead of broadcast.""" + # If we are missing the unique_id we should be able to fetch it + # from the device by doing a directed discovery at the host only + for device in await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT, host): + if device[FLUX_HOST] == host: + return device + return None + + +@callback +def async_trigger_discovery( + hass: HomeAssistant, + discovered_devices: list[dict[str, Any]], +) -> None: + """Trigger config flows for discovered devices.""" + for device in discovered_devices: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DISCOVERY}, + data=device, + ) + ) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the flux_led component.""" + domain_data = hass.data[DOMAIN] = {} + domain_data[FLUX_LED_DISCOVERY] = await async_discover_devices( + hass, STARTUP_SCAN_TIMEOUT + ) + + async def _async_discovery(*_: Any) -> None: + async_trigger_discovery( + hass, await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT) + ) + + async_trigger_discovery(hass, domain_data[FLUX_LED_DISCOVERY]) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_discovery) + async_track_time_interval(hass, _async_discovery, DISCOVERY_INTERVAL) + return True + + +async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Flux LED/MagicLight from a config entry.""" + host = entry.data[CONF_HOST] + if not entry.unique_id: + if discovery := await async_discover_device(hass, host): + async_update_entry_from_discovery(hass, entry, discovery) + + device: AIOWifiLedBulb = async_wifi_bulb_for_host(host) + signal = SIGNAL_STATE_UPDATED.format(device.ipaddr) + + @callback + def _async_state_changed(*_: Any) -> None: + _LOGGER.debug("%s: Device state updated: %s", device.ipaddr, device.raw_state) + async_dispatcher_send(hass, signal) + + try: + await device.async_setup(_async_state_changed) + except FLUX_LED_EXCEPTIONS as ex: + raise ConfigEntryNotReady( + str(ex) or f"Timed out trying to connect to {device.ipaddr}" + ) from ex + coordinator = FluxLedUpdateCoordinator(hass, device) + hass.data[DOMAIN][entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms( + entry, PLATFORMS_BY_TYPE[device.device_type] + ) + entry.async_on_unload(entry.add_update_listener(async_update_listener)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + device: AIOWifiLedBulb = hass.data[DOMAIN][entry.entry_id].device + platforms = PLATFORMS_BY_TYPE[device.device_type] + if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): + coordinator = hass.data[DOMAIN].pop(entry.entry_id) + await coordinator.device.async_stop() + return unload_ok + + +class FluxLedUpdateCoordinator(DataUpdateCoordinator): + """DataUpdateCoordinator to gather data for a specific flux_led device.""" + + def __init__( + self, + hass: HomeAssistant, + device: AIOWifiLedBulb, + ) -> None: + """Initialize DataUpdateCoordinator to gather data for specific device.""" + self.device = device + super().__init__( + hass, + _LOGGER, + name=self.device.ipaddr, + update_interval=timedelta(seconds=5), + # We don't want an immediate refresh since the device + # takes a moment to reflect the state change + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False + ), + ) + + async def _async_update_data(self) -> None: + """Fetch all device and sensor data from api.""" + try: + await self.device.async_update() + except FLUX_LED_EXCEPTIONS as ex: + raise UpdateFailed(ex) from ex diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py new file mode 100644 index 00000000000..306dbc2c25e --- /dev/null +++ b/homeassistant/components/flux_led/config_flow.py @@ -0,0 +1,259 @@ +"""Config flow for Flux LED/MagicLight.""" +from __future__ import annotations + +import logging +from typing import Any, Final + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODE, CONF_NAME, CONF_PROTOCOL +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import DiscoveryInfoType + +from . import ( + async_discover_device, + async_discover_devices, + async_update_entry_from_discovery, + async_wifi_bulb_for_host, +) +from .const import ( + CONF_CUSTOM_EFFECT_COLORS, + CONF_CUSTOM_EFFECT_SPEED_PCT, + CONF_CUSTOM_EFFECT_TRANSITION, + DEFAULT_EFFECT_SPEED, + DISCOVER_SCAN_TIMEOUT, + DOMAIN, + FLUX_HOST, + FLUX_LED_EXCEPTIONS, + FLUX_MAC, + FLUX_MODEL, + TRANSITION_GRADUAL, + TRANSITION_JUMP, + TRANSITION_STROBE, +) + +CONF_DEVICE: Final = "device" + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for FluxLED/MagicHome Integration.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovered_devices: dict[str, dict[str, Any]] = {} + self._discovered_device: dict[str, Any] = {} + + @staticmethod + @callback + def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> OptionsFlow: + """Get the options flow for the Flux LED component.""" + return OptionsFlow(config_entry) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle configuration via YAML import.""" + _LOGGER.debug("Importing configuration from YAML for flux_led") + host = user_input[CONF_HOST] + self._async_abort_entries_match({CONF_HOST: host}) + if mac := user_input[CONF_MAC]: + await self.async_set_unique_id(dr.format_mac(mac), raise_on_progress=False) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + return self.async_create_entry( + title=user_input[CONF_NAME], + data={ + CONF_HOST: host, + CONF_NAME: user_input[CONF_NAME], + CONF_PROTOCOL: user_input.get(CONF_PROTOCOL), + }, + options={ + CONF_MODE: user_input[CONF_MODE], + CONF_CUSTOM_EFFECT_COLORS: user_input[CONF_CUSTOM_EFFECT_COLORS], + CONF_CUSTOM_EFFECT_SPEED_PCT: user_input[CONF_CUSTOM_EFFECT_SPEED_PCT], + CONF_CUSTOM_EFFECT_TRANSITION: user_input[ + CONF_CUSTOM_EFFECT_TRANSITION + ], + }, + ) + + async def async_step_dhcp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + """Handle discovery via dhcp.""" + self._discovered_device = { + FLUX_HOST: discovery_info[IP_ADDRESS], + FLUX_MODEL: discovery_info[HOSTNAME], + FLUX_MAC: discovery_info[MAC_ADDRESS].replace(":", ""), + } + return await self._async_handle_discovery() + + async def async_step_discovery( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle discovery.""" + self._discovered_device = discovery_info + return await self._async_handle_discovery() + + async def _async_handle_discovery(self) -> FlowResult: + """Handle any discovery.""" + device = self._discovered_device + mac = dr.format_mac(device[FLUX_MAC]) + host = device[FLUX_HOST] + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_HOST] == host and not entry.unique_id: + async_update_entry_from_discovery(self.hass, entry, device) + return self.async_abort(reason="already_configured") + self.context[CONF_HOST] = host + for progress in self._async_in_progress(): + if progress.get("context", {}).get(CONF_HOST) == host: + return self.async_abort(reason="already_in_progress") + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + if user_input is not None: + return self._async_create_entry_from_device(self._discovered_device) + + self._set_confirm_only() + placeholders = self._discovered_device + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="discovery_confirm", description_placeholders=placeholders + ) + + @callback + def _async_create_entry_from_device(self, device: dict[str, Any]) -> FlowResult: + """Create a config entry from a device.""" + self._async_abort_entries_match({CONF_HOST: device[FLUX_HOST]}) + if device.get(FLUX_MAC): + name = f"{device[FLUX_MODEL]} {device[FLUX_MAC]}" + else: + name = device[FLUX_HOST] + return self.async_create_entry( + title=name, + data={ + CONF_HOST: device[FLUX_HOST], + CONF_NAME: name, + }, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + if user_input is not None: + if not (host := user_input[CONF_HOST]): + return await self.async_step_pick_device() + try: + device = await self._async_try_connect(host) + except FLUX_LED_EXCEPTIONS: + errors["base"] = "cannot_connect" + else: + if device[FLUX_MAC]: + await self.async_set_unique_id( + dr.format_mac(device[FLUX_MAC]), raise_on_progress=False + ) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + return self._async_create_entry_from_device(device) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Optional(CONF_HOST, default=""): str}), + errors=errors, + ) + + async def async_step_pick_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the step to pick discovered device.""" + if user_input is not None: + mac = user_input[CONF_DEVICE] + await self.async_set_unique_id(mac, raise_on_progress=False) + return self._async_create_entry_from_device(self._discovered_devices[mac]) + + current_unique_ids = self._async_current_ids() + current_hosts = { + entry.data[CONF_HOST] + for entry in self._async_current_entries(include_ignore=False) + } + discovered_devices = await async_discover_devices( + self.hass, DISCOVER_SCAN_TIMEOUT + ) + self._discovered_devices = { + dr.format_mac(device[FLUX_MAC]): device for device in discovered_devices + } + devices_name = { + mac: f"{device[FLUX_MODEL]} {mac} ({device[FLUX_HOST]})" + for mac, device in self._discovered_devices.items() + if mac not in current_unique_ids and device[FLUX_HOST] not in current_hosts + } + # Check if there is at least one device + if not devices_name: + return self.async_abort(reason="no_devices_found") + return self.async_show_form( + step_id="pick_device", + data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), + ) + + async def _async_try_connect(self, host: str) -> dict[str, Any]: + """Try to connect.""" + self._async_abort_entries_match({CONF_HOST: host}) + if device := await async_discover_device(self.hass, host): + return device + bulb = async_wifi_bulb_for_host(host) + try: + await bulb.async_setup(lambda: None) + finally: + await bulb.async_stop() + return {FLUX_MAC: None, FLUX_MODEL: None, FLUX_HOST: host} + + +class OptionsFlow(config_entries.OptionsFlow): + """Handle flux_led options.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize the flux_led options flow.""" + self._config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Configure the options.""" + errors: dict[str, str] = {} + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = self._config_entry.options + options_schema = vol.Schema( + { + vol.Optional( + CONF_CUSTOM_EFFECT_COLORS, + default=options.get(CONF_CUSTOM_EFFECT_COLORS, ""), + ): str, + vol.Optional( + CONF_CUSTOM_EFFECT_SPEED_PCT, + default=options.get( + CONF_CUSTOM_EFFECT_SPEED_PCT, DEFAULT_EFFECT_SPEED + ), + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional( + CONF_CUSTOM_EFFECT_TRANSITION, + default=options.get( + CONF_CUSTOM_EFFECT_TRANSITION, TRANSITION_GRADUAL + ), + ): vol.In([TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE]), + } + ) + + return self.async_show_form( + step_id="init", data_schema=options_schema, errors=errors + ) diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py new file mode 100644 index 00000000000..e7f9509c54b --- /dev/null +++ b/homeassistant/components/flux_led/const.py @@ -0,0 +1,57 @@ +"""Constants of the FluxLed/MagicHome Integration.""" + +import asyncio +import socket +from typing import Final + +DOMAIN: Final = "flux_led" + +API: Final = "flux_api" + +SIGNAL_STATE_UPDATED = "flux_led_{}_state_updated" + +CONF_AUTOMATIC_ADD: Final = "automatic_add" +DEFAULT_NETWORK_SCAN_INTERVAL: Final = 120 +DEFAULT_SCAN_INTERVAL: Final = 5 +DEFAULT_EFFECT_SPEED: Final = 50 + +FLUX_LED_DISCOVERY: Final = "flux_led_discovery" + +FLUX_LED_EXCEPTIONS: Final = ( + asyncio.TimeoutError, + socket.error, + RuntimeError, + BrokenPipeError, +) + +STARTUP_SCAN_TIMEOUT: Final = 5 +DISCOVER_SCAN_TIMEOUT: Final = 10 + +CONF_DEVICES: Final = "devices" +CONF_CUSTOM_EFFECT: Final = "custom_effect" +CONF_MODEL: Final = "model" + +MODE_AUTO: Final = "auto" +MODE_RGB: Final = "rgb" +MODE_RGBW: Final = "rgbw" + +# This mode enables white value to be controlled by brightness. +# RGB value is ignored when this mode is specified. +MODE_WHITE: Final = "w" + +TRANSITION_GRADUAL: Final = "gradual" +TRANSITION_JUMP: Final = "jump" +TRANSITION_STROBE: Final = "strobe" + +CONF_COLORS: Final = "colors" +CONF_SPEED_PCT: Final = "speed_pct" +CONF_TRANSITION: Final = "transition" + + +CONF_CUSTOM_EFFECT_COLORS: Final = "custom_effect_colors" +CONF_CUSTOM_EFFECT_SPEED_PCT: Final = "custom_effect_speed_pct" +CONF_CUSTOM_EFFECT_TRANSITION: Final = "custom_effect_transition" + +FLUX_HOST: Final = "ipaddr" +FLUX_MAC: Final = "id" +FLUX_MODEL: Final = "model" diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py new file mode 100644 index 00000000000..4183ccc14cd --- /dev/null +++ b/homeassistant/components/flux_led/entity.py @@ -0,0 +1,87 @@ +"""Support for FluxLED/MagicHome lights.""" +from __future__ import annotations + +from abc import abstractmethod +from typing import Any, cast + +from flux_led.aiodevice import AIOWifiLedBulb + +from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import FluxLedUpdateCoordinator +from .const import SIGNAL_STATE_UPDATED + + +class FluxEntity(CoordinatorEntity): + """Representation of a Flux entity.""" + + coordinator: FluxLedUpdateCoordinator + + def __init__( + self, + coordinator: FluxLedUpdateCoordinator, + unique_id: str | None, + name: str, + ) -> None: + """Initialize the light.""" + super().__init__(coordinator) + self._device: AIOWifiLedBulb = coordinator.device + self._responding = True + self._attr_name = name + self._attr_unique_id = unique_id + if self.unique_id: + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self.unique_id)}, + manufacturer="FluxLED/Magic Home", + model=self._device.model, + name=self.name, + sw_version=str(self._device.version_num), + ) + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return cast(bool, self._device.is_on) + + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return the attributes.""" + return {"ip_address": self._device.ipaddr} + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the specified device on.""" + await self._async_turn_on(**kwargs) + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + @abstractmethod + async def _async_turn_on(self, **kwargs: Any) -> None: + """Turn the specified device on.""" + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the specified device off.""" + await self._device.async_turn_off() + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if self.coordinator.last_update_success != self._responding: + self.async_write_ha_state() + self._responding = self.coordinator.last_update_success + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_STATE_UPDATED.format(self._device.ipaddr), + self.async_write_ha_state, + ) + ) + await super().async_added_to_hass() diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 2f8d2cc5536..f1fa4ed7dbb 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -1,76 +1,138 @@ -"""Support for Flux lights.""" +"""Support for FluxLED/MagicHome lights.""" +from __future__ import annotations + +import ast import logging import random +from typing import Any, Final, cast -from flux_led import BulbScanner, WifiLedBulb +from flux_led.const import ( + COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, + COLOR_MODE_DIM as FLUX_COLOR_MODE_DIM, + COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, + COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW, + COLOR_MODE_RGBWW as FLUX_COLOR_MODE_RGBWW, +) +from flux_led.utils import ( + color_temp_to_white_levels, + rgbcw_brightness, + rgbcw_to_rgbwc, + rgbw_brightness, + rgbww_brightness, +) import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_HS_COLOR, - ATTR_WHITE_VALUE, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + ATTR_WHITE, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_ONOFF, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + COLOR_MODE_WHITE, EFFECT_COLORLOOP, EFFECT_RANDOM, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, - SUPPORT_WHITE_VALUE, + SUPPORT_TRANSITION, LightEntity, ) -from homeassistant.const import ATTR_MODE, CONF_DEVICES, CONF_NAME, CONF_PROTOCOL +from homeassistant.const import ( + ATTR_MODE, + CONF_DEVICES, + CONF_HOST, + CONF_MAC, + CONF_MODE, + CONF_NAME, + CONF_PROTOCOL, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -import homeassistant.util.color as color_util +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.color import ( + color_hs_to_RGB, + color_RGB_to_hs, + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) + +from . import FluxLedUpdateCoordinator +from .const import ( + CONF_AUTOMATIC_ADD, + CONF_COLORS, + CONF_CUSTOM_EFFECT, + CONF_CUSTOM_EFFECT_COLORS, + CONF_CUSTOM_EFFECT_SPEED_PCT, + CONF_CUSTOM_EFFECT_TRANSITION, + CONF_SPEED_PCT, + CONF_TRANSITION, + DEFAULT_EFFECT_SPEED, + DOMAIN, + FLUX_HOST, + FLUX_LED_DISCOVERY, + FLUX_MAC, + MODE_AUTO, + MODE_RGB, + MODE_RGBW, + MODE_WHITE, + TRANSITION_GRADUAL, + TRANSITION_JUMP, + TRANSITION_STROBE, +) +from .entity import FluxEntity _LOGGER = logging.getLogger(__name__) -CONF_AUTOMATIC_ADD = "automatic_add" -CONF_CUSTOM_EFFECT = "custom_effect" -CONF_COLORS = "colors" -CONF_SPEED_PCT = "speed_pct" -CONF_TRANSITION = "transition" +SUPPORT_FLUX_LED: Final = SUPPORT_TRANSITION -DOMAIN = "flux_led" -SUPPORT_FLUX_LED = SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_COLOR +FLUX_COLOR_MODE_TO_HASS: Final = { + FLUX_COLOR_MODE_RGB: COLOR_MODE_RGB, + FLUX_COLOR_MODE_RGBW: COLOR_MODE_RGBW, + FLUX_COLOR_MODE_RGBWW: COLOR_MODE_RGBWW, + FLUX_COLOR_MODE_CCT: COLOR_MODE_COLOR_TEMP, +} -MODE_RGB = "rgb" -MODE_RGBW = "rgbw" - -# This mode enables white value to be controlled by brightness. -# RGB value is ignored when this mode is specified. -MODE_WHITE = "w" +EFFECT_SUPPORT_MODES = {COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW} # Constant color temp values for 2 flux_led special modes # Warm-white and Cool-white modes -COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF = 285 +COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: Final = 285 # List of supported effects which aren't already declared in LIGHT -EFFECT_RED_FADE = "red_fade" -EFFECT_GREEN_FADE = "green_fade" -EFFECT_BLUE_FADE = "blue_fade" -EFFECT_YELLOW_FADE = "yellow_fade" -EFFECT_CYAN_FADE = "cyan_fade" -EFFECT_PURPLE_FADE = "purple_fade" -EFFECT_WHITE_FADE = "white_fade" -EFFECT_RED_GREEN_CROSS_FADE = "rg_cross_fade" -EFFECT_RED_BLUE_CROSS_FADE = "rb_cross_fade" -EFFECT_GREEN_BLUE_CROSS_FADE = "gb_cross_fade" -EFFECT_COLORSTROBE = "colorstrobe" -EFFECT_RED_STROBE = "red_strobe" -EFFECT_GREEN_STROBE = "green_strobe" -EFFECT_BLUE_STROBE = "blue_strobe" -EFFECT_YELLOW_STROBE = "yellow_strobe" -EFFECT_CYAN_STROBE = "cyan_strobe" -EFFECT_PURPLE_STROBE = "purple_strobe" -EFFECT_WHITE_STROBE = "white_strobe" -EFFECT_COLORJUMP = "colorjump" -EFFECT_CUSTOM = "custom" +EFFECT_RED_FADE: Final = "red_fade" +EFFECT_GREEN_FADE: Final = "green_fade" +EFFECT_BLUE_FADE: Final = "blue_fade" +EFFECT_YELLOW_FADE: Final = "yellow_fade" +EFFECT_CYAN_FADE: Final = "cyan_fade" +EFFECT_PURPLE_FADE: Final = "purple_fade" +EFFECT_WHITE_FADE: Final = "white_fade" +EFFECT_RED_GREEN_CROSS_FADE: Final = "rg_cross_fade" +EFFECT_RED_BLUE_CROSS_FADE: Final = "rb_cross_fade" +EFFECT_GREEN_BLUE_CROSS_FADE: Final = "gb_cross_fade" +EFFECT_COLORSTROBE: Final = "colorstrobe" +EFFECT_RED_STROBE: Final = "red_strobe" +EFFECT_GREEN_STROBE: Final = "green_strobe" +EFFECT_BLUE_STROBE: Final = "blue_strobe" +EFFECT_YELLOW_STROBE: Final = "yellow_strobe" +EFFECT_CYAN_STROBE: Final = "cyan_strobe" +EFFECT_PURPLE_STROBE: Final = "purple_strobe" +EFFECT_WHITE_STROBE: Final = "white_strobe" +EFFECT_COLORJUMP: Final = "colorjump" +EFFECT_CUSTOM: Final = "custom" -EFFECT_MAP = { +EFFECT_MAP: Final = { EFFECT_COLORLOOP: 0x25, EFFECT_RED_FADE: 0x26, EFFECT_GREEN_FADE: 0x27, @@ -92,39 +154,34 @@ EFFECT_MAP = { EFFECT_WHITE_STROBE: 0x37, EFFECT_COLORJUMP: 0x38, } -EFFECT_CUSTOM_CODE = 0x60 +EFFECT_ID_NAME: Final = {v: k for k, v in EFFECT_MAP.items()} +EFFECT_CUSTOM_CODE: Final = 0x60 -TRANSITION_GRADUAL = "gradual" -TRANSITION_JUMP = "jump" -TRANSITION_STROBE = "strobe" +FLUX_EFFECT_LIST: Final = sorted(EFFECT_MAP) + [EFFECT_RANDOM] -FLUX_EFFECT_LIST = sorted(EFFECT_MAP) + [EFFECT_RANDOM] +SERVICE_CUSTOM_EFFECT: Final = "set_custom_effect" -CUSTOM_EFFECT_SCHEMA = vol.Schema( - { - vol.Required(CONF_COLORS): vol.All( - cv.ensure_list, - vol.Length(min=1, max=16), - [ - vol.All( - vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) - ) - ], - ), - vol.Optional(CONF_SPEED_PCT, default=50): vol.All( - vol.Range(min=0, max=100), vol.Coerce(int) - ), - vol.Optional(CONF_TRANSITION, default=TRANSITION_GRADUAL): vol.All( - cv.string, vol.In([TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE]) - ), - } -) +CUSTOM_EFFECT_DICT: Final = { + vol.Required(CONF_COLORS): vol.All( + cv.ensure_list, + vol.Length(min=1, max=16), + [vol.All(vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)))], + ), + vol.Optional(CONF_SPEED_PCT, default=50): vol.All( + vol.Range(min=0, max=100), vol.Coerce(int) + ), + vol.Optional(CONF_TRANSITION, default=TRANSITION_GRADUAL): vol.All( + cv.string, vol.In([TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE]) + ), +} -DEVICE_SCHEMA = vol.Schema( +CUSTOM_EFFECT_SCHEMA: Final = vol.Schema(CUSTOM_EFFECT_DICT) + +DEVICE_SCHEMA: Final = vol.Schema( { vol.Optional(CONF_NAME): cv.string, - vol.Optional(ATTR_MODE, default=MODE_RGBW): vol.All( - cv.string, vol.In([MODE_RGBW, MODE_RGB, MODE_WHITE]) + vol.Optional(ATTR_MODE, default=MODE_AUTO): vol.All( + cv.string, vol.In([MODE_AUTO, MODE_RGBW, MODE_RGB, MODE_WHITE]) ), vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(["ledenet"])), vol.Optional(CONF_CUSTOM_EFFECT): CUSTOM_EFFECT_SCHEMA, @@ -139,236 +196,292 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +def _flux_color_mode_to_hass(flux_color_mode: str, flux_color_modes: set[str]) -> str: + """Map the flux color mode to Home Assistant color mode.""" + if flux_color_mode == FLUX_COLOR_MODE_DIM: + if len(flux_color_modes) > 1: + return COLOR_MODE_WHITE + return COLOR_MODE_BRIGHTNESS + return FLUX_COLOR_MODE_TO_HASS.get(flux_color_mode, COLOR_MODE_ONOFF) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> bool: + """Set up the flux led platform.""" + domain_data = hass.data[DOMAIN] + discovered_mac_by_host = { + device[FLUX_HOST]: device[FLUX_MAC] + for device in domain_data[FLUX_LED_DISCOVERY] + } + for host, device_config in config.get(CONF_DEVICES, {}).items(): + _LOGGER.warning( + "Configuring flux_led via yaml is deprecated; the configuration for" + " %s has been migrated to a config entry and can be safely removed", + host, + ) + custom_effects = device_config.get(CONF_CUSTOM_EFFECT, {}) + custom_effect_colors = None + if CONF_COLORS in custom_effects: + custom_effect_colors = str(custom_effects[CONF_COLORS]) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: host, + CONF_MAC: discovered_mac_by_host.get(host), + CONF_NAME: device_config[CONF_NAME], + CONF_PROTOCOL: device_config.get(CONF_PROTOCOL), + CONF_MODE: device_config.get(ATTR_MODE, MODE_AUTO), + CONF_CUSTOM_EFFECT_COLORS: custom_effect_colors, + CONF_CUSTOM_EFFECT_SPEED_PCT: custom_effects.get( + CONF_SPEED_PCT, DEFAULT_EFFECT_SPEED + ), + CONF_CUSTOM_EFFECT_TRANSITION: custom_effects.get( + CONF_TRANSITION, TRANSITION_GRADUAL + ), + }, + ) + ) + return True + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Flux lights.""" - lights = [] - light_ips = [] + coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - for ipaddr, device_config in config.get(CONF_DEVICES, {}).items(): - device = {} - device["name"] = device_config[CONF_NAME] - device["ipaddr"] = ipaddr - device[CONF_PROTOCOL] = device_config.get(CONF_PROTOCOL) - device[ATTR_MODE] = device_config[ATTR_MODE] - device[CONF_CUSTOM_EFFECT] = device_config.get(CONF_CUSTOM_EFFECT) - light = FluxLight(device) - lights.append(light) - light_ips.append(ipaddr) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_CUSTOM_EFFECT, + CUSTOM_EFFECT_DICT, + "async_set_custom_effect", + ) + options = entry.options - if not config.get(CONF_AUTOMATIC_ADD, False): - add_entities(lights, True) - return + try: + custom_effect_colors = ast.literal_eval( + options.get(CONF_CUSTOM_EFFECT_COLORS) or "[]" + ) + except (ValueError, TypeError, SyntaxError, MemoryError) as ex: + _LOGGER.warning( + "Could not parse custom effect colors for %s: %s", entry.unique_id, ex + ) + custom_effect_colors = [] - # Find the bulbs on the LAN - scanner = BulbScanner() - scanner.scan(timeout=10) - for device in scanner.getBulbInfo(): - ipaddr = device["ipaddr"] - if ipaddr in light_ips: - continue - device["name"] = f"{device['id']} {ipaddr}" - device[ATTR_MODE] = None - device[CONF_PROTOCOL] = None - device[CONF_CUSTOM_EFFECT] = None - light = FluxLight(device) - lights.append(light) - - add_entities(lights, True) + async_add_entities( + [ + FluxLight( + coordinator, + entry.unique_id, + entry.data[CONF_NAME], + list(custom_effect_colors), + options.get(CONF_CUSTOM_EFFECT_SPEED_PCT, DEFAULT_EFFECT_SPEED), + options.get(CONF_CUSTOM_EFFECT_TRANSITION, TRANSITION_GRADUAL), + ) + ] + ) -class FluxLight(LightEntity): +class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): """Representation of a Flux light.""" - def __init__(self, device): + def __init__( + self, + coordinator: FluxLedUpdateCoordinator, + unique_id: str | None, + name: str, + custom_effect_colors: list[tuple[int, int, int]], + custom_effect_speed_pct: int, + custom_effect_transition: str, + ) -> None: """Initialize the light.""" - self._name = device["name"] - self._ipaddr = device["ipaddr"] - self._protocol = device[CONF_PROTOCOL] - self._mode = device[ATTR_MODE] - self._custom_effect = device[CONF_CUSTOM_EFFECT] - self._bulb = None - self._error_reported = False - - def _connect(self): - """Connect to Flux light.""" - - self._bulb = WifiLedBulb(self._ipaddr, timeout=5) - if self._protocol: - self._bulb.setProtocol(self._protocol) - - # After bulb object is created the status is updated. We can - # now set the correct mode if it was not explicitly defined. - if not self._mode: - if self._bulb.rgbwcapable: - self._mode = MODE_RGBW - else: - self._mode = MODE_RGB - - def _disconnect(self): - """Disconnect from Flux light.""" - self._bulb = None + super().__init__(coordinator, unique_id, name) + self._attr_supported_features = SUPPORT_FLUX_LED + self._attr_min_mireds = ( + color_temperature_kelvin_to_mired(self._device.max_temp) + 1 + ) # for rounding + self._attr_max_mireds = color_temperature_kelvin_to_mired(self._device.min_temp) + self._attr_supported_color_modes = { + _flux_color_mode_to_hass(mode, self._device.color_modes) + for mode in self._device.color_modes + } + if self._attr_supported_color_modes.intersection(EFFECT_SUPPORT_MODES): + self._attr_supported_features |= SUPPORT_EFFECT + self._attr_effect_list = FLUX_EFFECT_LIST + if custom_effect_colors: + self._attr_effect_list = [*FLUX_EFFECT_LIST, EFFECT_CUSTOM] + self._custom_effect_colors = custom_effect_colors + self._custom_effect_speed_pct = custom_effect_speed_pct + self._custom_effect_transition = custom_effect_transition @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._bulb is not None - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._bulb.isOn() - - @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - if self._mode == MODE_WHITE: - return self.white_value - - return self._bulb.brightness + return cast(int, self._device.brightness) @property - def hs_color(self): - """Return the color property.""" - return color_util.color_RGB_to_hs(*self._bulb.getRgb()) + def color_temp(self) -> int: + """Return the kelvin value of this light in mired.""" + return color_temperature_kelvin_to_mired(self._device.color_temp) @property - def supported_features(self): - """Flag supported features.""" - if self._mode == MODE_RGBW: - return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE | SUPPORT_COLOR_TEMP - - if self._mode == MODE_WHITE: - return SUPPORT_BRIGHTNESS - - return SUPPORT_FLUX_LED + def rgb_color(self) -> tuple[int, int, int]: + """Return the rgb color value.""" + # Note that we call color_RGB_to_hs and not color_RGB_to_hsv + # to get the unscaled value since this is what the frontend wants + # https://github.com/home-assistant/frontend/blob/e797c017614797bb11671496d6bd65863de22063/src/dialogs/more-info/controls/more-info-light.ts#L263 + rgb: tuple[int, int, int] = color_hs_to_RGB(*color_RGB_to_hs(*self._device.rgb)) + return rgb @property - def white_value(self): - """Return the white value of this light between 0..255.""" - return self._bulb.getRgbw()[3] + def rgbw_color(self) -> tuple[int, int, int, int]: + """Return the rgbw color value.""" + rgbw: tuple[int, int, int, int] = self._device.rgbw + return rgbw @property - def effect_list(self): - """Return the list of supported effects.""" - if self._custom_effect: - return FLUX_EFFECT_LIST + [EFFECT_CUSTOM] - - return FLUX_EFFECT_LIST + def rgbww_color(self) -> tuple[int, int, int, int, int]: + """Return the rgbww aka rgbcw color value.""" + rgbcw: tuple[int, int, int, int, int] = self._device.rgbcw + return rgbcw @property - def effect(self): + def rgbwc_color(self) -> tuple[int, int, int, int, int]: + """Return the rgbwc color value.""" + rgbwc: tuple[int, int, int, int, int] = self._device.rgbww + return rgbwc + + @property + def color_mode(self) -> str: + """Return the color mode of the light.""" + return _flux_color_mode_to_hass( + self._device.color_mode, self._device.color_modes + ) + + @property + def effect(self) -> str | None: """Return the current effect.""" - current_mode = self._bulb.raw_state[3] - - if current_mode == EFFECT_CUSTOM_CODE: + if (current_mode := self._device.preset_pattern_num) == EFFECT_CUSTOM_CODE: return EFFECT_CUSTOM + return EFFECT_ID_NAME.get(current_mode) - for effect, code in EFFECT_MAP.items(): - if current_mode == code: - return effect - - return None - - def turn_on(self, **kwargs): + async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" if not self.is_on: - self._bulb.turnOn() - - hs_color = kwargs.get(ATTR_HS_COLOR) - - if hs_color: - rgb = color_util.color_hs_to_RGB(*hs_color) - else: - rgb = None - - brightness = kwargs.get(ATTR_BRIGHTNESS) - effect = kwargs.get(ATTR_EFFECT) - white = kwargs.get(ATTR_WHITE_VALUE) - color_temp = kwargs.get(ATTR_COLOR_TEMP) - - # handle special modes - if color_temp is not None: - if brightness is None: - brightness = self.brightness - if color_temp > COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: - self._bulb.setRgbw(w=brightness) - else: - self._bulb.setRgbw(w2=brightness) - return - - # Show warning if effect set with rgb, brightness, or white level - if effect and (brightness or white or rgb): - _LOGGER.warning( - "RGB, brightness and white level are ignored when" - " an effect is specified for a flux bulb" - ) - - # Random color effect - if effect == EFFECT_RANDOM: - self._bulb.setRgb( - random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) - ) - return - - if effect == EFFECT_CUSTOM: - if self._custom_effect: - self._bulb.setCustomPattern( - self._custom_effect[CONF_COLORS], - self._custom_effect[CONF_SPEED_PCT], - self._custom_effect[CONF_TRANSITION], - ) - return - - # Effect selection - if effect in EFFECT_MAP: - self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) - return - - # Preserve current brightness on color/white level change - if brightness is None: - brightness = self.brightness - - # Preserve color on brightness/white level change - if rgb is None: - rgb = self._bulb.getRgb() - - if white is None and self._mode == MODE_RGBW: - white = self.white_value - - # handle W only mode (use brightness instead of white value) - if self._mode == MODE_WHITE: - self._bulb.setRgbw(0, 0, 0, w=brightness) - - # handle RGBW mode - elif self._mode == MODE_RGBW: - self._bulb.setRgbw(*tuple(rgb), w=white, brightness=brightness) - - # handle RGB mode - else: - self._bulb.setRgb(*tuple(rgb), brightness=brightness) - - def turn_off(self, **kwargs): - """Turn the specified or all lights off.""" - self._bulb.turnOff() - - def update(self): - """Synchronize state with bulb.""" - if not self.available: - try: - self._connect() - self._error_reported = False - except OSError: - self._disconnect() - if not self._error_reported: - _LOGGER.warning( - "Failed to connect to bulb %s, %s", self._ipaddr, self._name - ) - self._error_reported = True + await self._device.async_turn_on() + if not kwargs: return - self._bulb.update_state(retry=2) + if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None: + brightness = self.brightness + + # Handle switch to CCT Color Mode + if ATTR_COLOR_TEMP in kwargs: + color_temp_mired = kwargs[ATTR_COLOR_TEMP] + color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired) + if self.color_mode != COLOR_MODE_RGBWW: + await self._device.async_set_white_temp(color_temp_kelvin, brightness) + return + + # When switching to color temp from RGBWW mode, + # we do not want the overall brightness, we only + # want the brightness of the white channels + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._device.getWhiteTemperature()[1] + ) + cold, warm = color_temp_to_white_levels(color_temp_kelvin, brightness) + await self._device.async_set_levels(r=0, b=0, g=0, w=warm, w2=cold) + return + # Handle switch to RGB Color Mode + if ATTR_RGB_COLOR in kwargs: + await self._device.async_set_levels( + *kwargs[ATTR_RGB_COLOR], brightness=brightness + ) + return + # Handle switch to RGBW Color Mode + if ATTR_RGBW_COLOR in kwargs: + if ATTR_BRIGHTNESS in kwargs: + rgbw = rgbw_brightness(kwargs[ATTR_RGBW_COLOR], brightness) + else: + rgbw = kwargs[ATTR_RGBW_COLOR] + await self._device.async_set_levels(*rgbw) + return + # Handle switch to RGBWW Color Mode + if ATTR_RGBWW_COLOR in kwargs: + if ATTR_BRIGHTNESS in kwargs: + rgbcw = rgbcw_brightness(kwargs[ATTR_RGBWW_COLOR], brightness) + else: + rgbcw = kwargs[ATTR_RGBWW_COLOR] + await self._device.async_set_levels(*rgbcw_to_rgbwc(rgbcw)) + return + if ATTR_WHITE in kwargs: + await self._device.async_set_levels(w=kwargs[ATTR_WHITE]) + return + if ATTR_EFFECT in kwargs: + effect = kwargs[ATTR_EFFECT] + # Random color effect + if effect == EFFECT_RANDOM: + await self._device.async_set_levels( + random.randint(0, 255), + random.randint(0, 255), + random.randint(0, 255), + ) + return + # Custom effect + if effect == EFFECT_CUSTOM: + if self._custom_effect_colors: + await self._device.async_set_custom_pattern( + self._custom_effect_colors, + self._custom_effect_speed_pct, + self._custom_effect_transition, + ) + return + # Effect selection + if effect in EFFECT_MAP: + await self._device.async_set_preset_pattern( + EFFECT_MAP[effect], DEFAULT_EFFECT_SPEED + ) + return + raise ValueError(f"Unknown effect {effect}") + # Handle brightness adjustment in CCT Color Mode + if self.color_mode == COLOR_MODE_COLOR_TEMP: + await self._device.async_set_white_temp(self._device.color_temp, brightness) + return + # Handle brightness adjustment in RGB Color Mode + if self.color_mode == COLOR_MODE_RGB: + await self._device.async_set_levels(*self.rgb_color, brightness=brightness) + return + # Handle brightness adjustment in RGBW Color Mode + if self.color_mode == COLOR_MODE_RGBW: + await self._device.async_set_levels( + *rgbw_brightness(self.rgbw_color, brightness) + ) + return + # Handle brightness adjustment in RGBWW Color Mode + if self.color_mode == COLOR_MODE_RGBWW: + rgbwc = self.rgbwc_color + await self._device.async_set_levels(*rgbww_brightness(rgbwc, brightness)) + return + # Handle Brightness Only Color Mode + if self.color_mode in {COLOR_MODE_WHITE, COLOR_MODE_BRIGHTNESS}: + await self._device.async_set_levels(w=brightness) + return + raise ValueError(f"Unsupported color mode {self.color_mode}") + + async def async_set_custom_effect( + self, colors: list[tuple[int, int, int]], speed_pct: int, transition: str + ) -> None: + """Set a custom effect on the bulb.""" + await self._device.async_set_custom_pattern( + colors, + speed_pct, + transition, + ) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 0c6d8ae8db1..4b5a63542c9 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -1,8 +1,76 @@ { "domain": "flux_led", - "name": "Flux LED/MagicLight", + "name": "Flux LED/MagicHome", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.22"], - "codeowners": [], - "iot_class": "local_polling" + "requirements": ["flux_led==0.24.13"], + "quality_scale": "platinum", + "codeowners": ["@icemanch"], + "iot_class": "local_push", + "dhcp": [ + { + "macaddress": "18B905*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "249494*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "7CB94C*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "B4E842*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "8CCE4E*", + "hostname": "lwip*" + }, + { + "hostname": "zengge_0[6789b]_*" + }, + { + "hostname": "zengge_1[06789abc]_*" + }, + { + "hostname": "zengge_2[15]_*" + }, + { + "hostname": "zengge_3[35]_*" + }, + { + "hostname": "zengge_4[14]_*" + }, + { + "hostname": "zengge_5[24]_*" + }, + { + "hostname": "zengge_62_*" + }, + { + "hostname": "zengge_81_*" + }, + { + "hostname": "zengge_0[0e]_*" + }, + { + "hostname": "zengge_9[34567]_*" + }, + { + "hostname": "zengge_a[123]_*" + }, + { + "hostname": "zengge_d1_*" + }, + { + "hostname": "zengge_e[12]_*" + }, + { + "macaddress": "C82E47*", + "hostname": "sta*" + } + ] } + diff --git a/homeassistant/components/flux_led/services.yaml b/homeassistant/components/flux_led/services.yaml new file mode 100644 index 00000000000..f1dae55560b --- /dev/null +++ b/homeassistant/components/flux_led/services.yaml @@ -0,0 +1,38 @@ +set_custom_effect: + description: Set a custom light effect. + target: + entity: + integration: flux_led + domain: light + fields: + colors: + description: List of colors for the custom effect (RGB). (Max 16 Colors) + example: | + - [255,0,0] + - [0,255,0] + - [0,0,255] + required: true + selector: + object: + speed_pct: + description: Effect speed for the custom effect (0-100). + example: 80 + default: 50 + required: false + selector: + number: + min: 1 + step: 1 + max: 100 + unit_of_measurement: "%" + transition: + description: Effect transition. + example: 'jump' + default: 'gradual' + required: false + selector: + select: + options: + - "gradual" + - "jump" + - "strobe" diff --git a/homeassistant/components/flux_led/strings.json b/homeassistant/components/flux_led/strings.json new file mode 100644 index 00000000000..f311f559589 --- /dev/null +++ b/homeassistant/components/flux_led/strings.json @@ -0,0 +1,36 @@ +{ + "config": { + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "user": { + "description": "If you leave the host empty, discovery will be used to find devices.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "discovery_confirm": { + "description": "Do you want to setup {model} {id} ({ipaddr})?" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "The chosen brightness mode.", + "custom_effect_colors": "Custom Effect: List of 1 to 16 [R,G,B] colors. Example: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Custom Effect: Speed in percents for the effect that switch colors.", + "custom_effect_transition": "Custom Effect: Type of transition between the colors." + } + } + } + } +} diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py new file mode 100644 index 00000000000..0ca7a771c78 --- /dev/null +++ b/homeassistant/components/flux_led/switch.py @@ -0,0 +1,42 @@ +"""Support for FluxLED/MagicHome switches.""" +from __future__ import annotations + +from typing import Any + +from homeassistant import config_entries +from homeassistant.components.switch import SwitchEntity +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import FluxLedUpdateCoordinator +from .const import DOMAIN +from .entity import FluxEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Flux lights.""" + coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [ + FluxSwitch( + coordinator, + entry.unique_id, + entry.data[CONF_NAME], + ) + ] + ) + + +class FluxSwitch(FluxEntity, CoordinatorEntity, SwitchEntity): + """Representation of a Flux switch.""" + + async def _async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + if not self.is_on: + await self._device.async_turn_on() diff --git a/homeassistant/components/flux_led/translations/bg.json b/homeassistant/components/flux_led/translations/bg.json new file mode 100644 index 00000000000..462548016e5 --- /dev/null +++ b/homeassistant/components/flux_led/translations/bg.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {model} {id} ({ipaddr})?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0410\u043a\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u0435 \u0445\u043e\u0441\u0442\u0430 \u043f\u0440\u0430\u0437\u0435\u043d, \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435\u0442\u043e \u0449\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0437\u0430 \u043d\u0430\u043c\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d \u0435\u0444\u0435\u043a\u0442: \u0421\u043f\u0438\u0441\u044a\u043a \u043e\u0442 1 \u0434\u043e 16 [R,G,B] \u0446\u0432\u044f\u0442\u0430. \u041f\u0440\u0438\u043c\u0435\u0440: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d \u0435\u0444\u0435\u043a\u0442: \u0421\u043a\u043e\u0440\u043e\u0441\u0442 \u0432 \u043f\u0440\u043e\u0446\u0435\u043d\u0442\u0438 \u0437\u0430 \u0435\u0444\u0435\u043a\u0442\u0430, \u043a\u043e\u0439\u0442\u043e \u043f\u0440\u0435\u0432\u043a\u043b\u044e\u0447\u0432\u0430 \u0446\u0432\u0435\u0442\u043e\u0432\u0435\u0442\u0435.", + "custom_effect_transition": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d \u0435\u0444\u0435\u043a\u0442: \u0422\u0438\u043f \u043f\u0440\u0435\u0445\u043e\u0434 \u043c\u0435\u0436\u0434\u0443 \u0446\u0432\u0435\u0442\u043e\u0432\u0435\u0442\u0435.", + "mode": "\u0418\u0437\u0431\u0440\u0430\u043d\u0438\u044f\u0442 \u0440\u0435\u0436\u0438\u043c \u043d\u0430 \u044f\u0440\u043a\u043e\u0441\u0442." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/cs.json b/homeassistant/components/flux_led/translations/cs.json new file mode 100644 index 00000000000..542a503a360 --- /dev/null +++ b/homeassistant/components/flux_led/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/fr.json b/homeassistant/components/flux_led/translations/fr.json new file mode 100644 index 00000000000..9cb1d7dfd16 --- /dev/null +++ b/homeassistant/components/flux_led/translations/fr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/he.json b/homeassistant/components/flux_led/translations/he.json new file mode 100644 index 00000000000..aa2d7877791 --- /dev/null +++ b/homeassistant/components/flux_led/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/hu.json b/homeassistant/components/flux_led/translations/hu.json index 3cfef2c9eb6..1208f87fe70 100644 --- a/homeassistant/components/flux_led/translations/hu.json +++ b/homeassistant/components/flux_led/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { @@ -11,7 +11,7 @@ "flow_title": "{model} {id} ({ipaddr})", "step": { "discovery_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a(z) {model} {id} ({ipaddr}) webhelyet?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {model} {id} ({ipaddr}) ?" }, "user": { "data": { diff --git a/homeassistant/components/flux_led/translations/is.json b/homeassistant/components/flux_led/translations/is.json new file mode 100644 index 00000000000..89f72dd4362 --- /dev/null +++ b/homeassistant/components/flux_led/translations/is.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "T\u00e6ki n\u00fa \u00feegar stillt", + "no_devices_found": "Engin t\u00e6ki fundust \u00e1 netinu" + }, + "error": { + "cannot_connect": "Tenging mist\u00f3kst" + }, + "flow_title": "{model} {id} ( {ipaddr} )", + "step": { + "discovery_confirm": { + "description": "Viltu setja upp {model} {id} ( {ipaddr} )?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Ef \u00fe\u00fa skilur host eftir autt ver\u00f0ur leit notu\u00f0 til a\u00f0 finna t\u00e6ki" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "S\u00e9rsni\u00f0in \u00e1hrif: Listi yfir 1 til 16 [R, G, B] liti. D\u00e6mi: [255,0,255], [60,128,0]", + "custom_effect_speed_pct": "S\u00e9rsni\u00f0in \u00e1hrif: Hra\u00f0i \u00ed pr\u00f3sentum fyrir \u00e1hrifin sem skipta um liti.", + "custom_effect_transition": "S\u00e9rsni\u00f0in \u00e1hrif: Ger\u00f0 bl\u00f6ndunar \u00e1 milli litanna.", + "mode": "Valinn birtuhamur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/pl.json b/homeassistant/components/flux_led/translations/pl.json new file mode 100644 index 00000000000..2e749564860 --- /dev/null +++ b/homeassistant/components/flux_led/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/tr.json b/homeassistant/components/flux_led/translations/tr.json new file mode 100644 index 00000000000..3be9b8e3c26 --- /dev/null +++ b/homeassistant/components/flux_led/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index a8b084eb801..c243c0d45c8 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.5"], + "requirements": ["watchdog==2.1.6"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling" diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index 9638ea4e4dd..1fd77b9797c 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -25,11 +25,9 @@ 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 + api_key = entry.options.get(CONF_API_KEY) or None session = async_get_clientsession(hass) forecast = ForecastSolar( diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index ea76ed7da2a..1ec8c3e4df1 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -21,7 +21,6 @@ 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: tuple[ForecastSolarSensorEntityDescription, ...] = ( diff --git a/homeassistant/components/forecast_solar/energy.py b/homeassistant/components/forecast_solar/energy.py index 6bf63910e5f..33537396330 100644 --- a/homeassistant/components/forecast_solar/energy.py +++ b/homeassistant/components/forecast_solar/energy.py @@ -10,9 +10,7 @@ async def async_get_solar_forecast( hass: HomeAssistant, config_entry_id: str ) -> dict[str, dict[str, float | int]] | None: """Get solar forecast for a config entry ID.""" - coordinator = hass.data[DOMAIN].get(config_entry_id) - - if coordinator is None: + if (coordinator := hass.data[DOMAIN].get(config_entry_id)) is None: return None return { diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index 2ad86186652..cd672311c52 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -5,13 +5,8 @@ from datetime import datetime 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_MODEL, - ATTR_NAME, -) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( @@ -19,7 +14,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import ATTR_ENTRY_TYPE, DOMAIN, ENTRY_TYPE_SERVICE, SENSORS +from .const import DOMAIN, ENTRY_TYPE_SERVICE, SENSORS from .models import ForecastSolarSensorEntityDescription @@ -57,13 +52,13 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): self.entity_id = f"{SENSOR_DOMAIN}.{entity_description.key}" self._attr_unique_id = f"{entry_id}_{entity_description.key}" - self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, entry_id)}, - ATTR_NAME: "Solar Production Forecast", - ATTR_MANUFACTURER: "Forecast.Solar", - ATTR_MODEL: coordinator.data.account_type.value, - ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE, - } + self._attr_device_info = DeviceInfo( + entry_type=ENTRY_TYPE_SERVICE, + identifiers={(DOMAIN, entry_id)}, + manufacturer="Forecast.Solar", + model=coordinator.data.account_type.value, + name="Solar Production Forecast", + ) @property def native_value(self) -> StateType: diff --git a/homeassistant/components/forecast_solar/translations/bg.json b/homeassistant/components/forecast_solar/translations/bg.json new file mode 100644 index 00000000000..289146783a4 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "\u0410\u0437\u0438\u043c\u0443\u0442 (360 \u0433\u0440\u0430\u0434\u0443\u0441\u0430, 0 = \u0421\u0435\u0432\u0435\u0440, 90 = \u0418\u0437\u0442\u043e\u043a, 180 = \u042e\u0433, 270 = \u0417\u0430\u043f\u0430\u0434)", + "declination": "\u0414\u0435\u043a\u043b\u0438\u043d\u0430\u0446\u0438\u044f (0 = \u0445\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u043d\u043e, 90 = \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u043d\u043e)", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "azimuth": "\u0410\u0437\u0438\u043c\u0443\u0442 (360 \u0433\u0440\u0430\u0434\u0443\u0441\u0430, 0 = \u0421\u0435\u0432\u0435\u0440, 90 = \u0418\u0437\u0442\u043e\u043a, 180 = \u042e\u0433, 270 = \u0417\u0430\u043f\u0430\u0434)", + "declination": "\u0414\u0435\u043a\u043b\u0438\u043d\u0430\u0446\u0438\u044f (0 = \u0445\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u043d\u043e, 90 = \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u043d\u043e)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 724db80fabd..aeb2350ce22 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -621,8 +621,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): @property def media_image_url(self): """Image url of current playing media.""" - url = self._track_info.get("artwork_url") - if url: + if url := self._track_info.get("artwork_url"): url = self._api.full_url(url) return url @@ -769,11 +768,10 @@ class ForkedDaapdUpdater: async def async_init(self): """Perform async portion of class initialization.""" server_config = await self._api.get_request("config") - websocket_port = server_config.get("websocket_port") - if websocket_port: + if websocket_port := server_config.get("websocket_port"): self.websocket_handler = asyncio.create_task( self._api.start_websocket_handler( - server_config["websocket_port"], + websocket_port, WS_NOTIFY_EVENT_TYPES, self._update, WEBSOCKET_RECONNECT_TIME, diff --git a/homeassistant/components/forked_daapd/translations/bg.json b/homeassistant/components/forked_daapd/translations/bg.json new file mode 100644 index 00000000000..5a415dfbee2 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "unknown_error": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "wrong_host_or_port": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430.", + "wrong_password": "\u0413\u0440\u0435\u0448\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430." + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041f\u0440\u0438\u044f\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "password": "API \u043f\u0430\u0440\u043e\u043b\u0430 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u043d\u044f\u043c\u0430 \u043f\u0430\u0440\u043e\u043b\u0430)", + "port": "API \u043f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/tr.json b/homeassistant/components/forked_daapd/translations/tr.json index cf354c5c87f..1ca2f9d4715 100644 --- a/homeassistant/components/forked_daapd/translations/tr.json +++ b/homeassistant/components/forked_daapd/translations/tr.json @@ -11,9 +11,9 @@ "user": { "data": { "host": "Ana Bilgisayar", - "name": "Kolay ad", + "name": "Kolay Ad\u0131", "password": "API parolas\u0131 (parola yoksa bo\u015f b\u0131rak\u0131n)", - "port": "API ba\u011flant\u0131 noktas\u0131" + "port": "API Port" } } } diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index 1b9134bee44..a1507af99dc 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -102,9 +102,7 @@ class FortiOSDeviceScanner(DeviceScanner): device = device.lower() - data = self._clients_json - - if data == 0: + if (data := self._clients_json) == 0: _LOGGER.error("No json results to get device names") return None diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 9d825ed0851..a6714094b0c 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py index 6dc0d1c8228..59f3811a14b 100644 --- a/homeassistant/components/foursquare/__init__.py +++ b/homeassistant/components/foursquare/__init__.py @@ -6,7 +6,7 @@ import requests import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_CREATED, HTTP_OK +from homeassistant.const import CONF_ACCESS_TOKEN import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -56,7 +56,7 @@ def setup(hass, config): url = f"https://api.foursquare.com/v2/checkins/add?oauth_token={config[CONF_ACCESS_TOKEN]}&v=20160802&m=swarm" response = requests.post(url, data=call.data, timeout=10) - if response.status_code not in (HTTP_OK, HTTP_CREATED): + if response.status_code not in (HTTPStatus.OK, HTTPStatus.CREATED): _LOGGER.exception( "Error checking in user. Response %d: %s:", response.status_code, diff --git a/homeassistant/components/free_mobile/notify.py b/homeassistant/components/free_mobile/notify.py index a4351bfe678..61733237807 100644 --- a/homeassistant/components/free_mobile/notify.py +++ b/homeassistant/components/free_mobile/notify.py @@ -1,17 +1,12 @@ """Support for Free Mobile SMS platform.""" +from http import HTTPStatus import logging from freesms import FreeClient import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_USERNAME, - HTTP_BAD_REQUEST, - HTTP_FORBIDDEN, - HTTP_INTERNAL_SERVER_ERROR, -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -37,11 +32,11 @@ class FreeSMSNotificationService(BaseNotificationService): """Send a message to the Free Mobile user cell.""" resp = self.free_client.send_sms(message) - if resp.status_code == HTTP_BAD_REQUEST: + if resp.status_code == HTTPStatus.BAD_REQUEST: _LOGGER.error("At least one parameter is missing") - elif resp.status_code == 402: + elif resp.status_code == HTTPStatus.PAYMENT_REQUIRED: _LOGGER.error("Too much SMS send in a few time") - elif resp.status_code == HTTP_FORBIDDEN: + elif resp.status_code == HTTPStatus.FORBIDDEN: _LOGGER.error("Wrong Username/Password") - elif resp.status_code == HTTP_INTERNAL_SERVER_ERROR: + elif resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR: _LOGGER.error("Server error, try later") diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index bb308e154ef..c343e8d629c 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 2ad262dd2bd..38a781c8c12 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -114,12 +114,12 @@ class FreeboxDevice(ScannerEntity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": self._manufacturer, - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + identifiers={(DOMAIN, self.unique_id)}, + manufacturer=self._manufacturer, + name=self.name, + ) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 0673d550d76..e352146915e 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -183,13 +183,14 @@ class FreeboxRouter: @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, - "identifiers": {(DOMAIN, self.mac)}, - "name": self.name, - "manufacturer": "Freebox SAS", - "sw_version": self._sw_v, - } + return DeviceInfo( + configuration_url=f"https://{self._host}:{self._port}/", + connections={(CONNECTION_NETWORK_MAC, self.mac)}, + identifiers={(DOMAIN, self.mac)}, + manufacturer="Freebox SAS", + name=self.name, + sw_version=self._sw_v, + ) @property def signal_device_new(self) -> str: diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 654a73b786c..016434ac89f 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -163,20 +163,23 @@ class FreeboxDiskSensor(FreeboxSensor): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "identifiers": {(DOMAIN, self._disk["id"])}, - "name": f"Disk {self._disk['id']}", - "model": self._disk["model"], - "sw_version": self._disk["firmware"], - "via_device": ( + return DeviceInfo( + identifiers={(DOMAIN, self._disk["id"])}, + model=self._disk["model"], + name=f"Disk {self._disk['id']}", + sw_version=self._disk["firmware"], + via_device=( DOMAIN, self._router.mac, ), - } + ) @callback def async_update_state(self) -> None: """Update the Freebox disk sensor.""" - self._attr_native_value = round( - self._partition["free_bytes"] * 100 / self._partition["total_bytes"], 2 - ) + value = None + if self._partition.get("total_bytes"): + value = round( + self._partition["free_bytes"] * 100 / self._partition["total_bytes"], 2 + ) + self._attr_native_value = value diff --git a/homeassistant/components/freebox/translations/bg.json b/homeassistant/components/freebox/translations/bg.json new file mode 100644 index 00000000000..c8526b8367d --- /dev/null +++ b/homeassistant/components/freebox/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "register_failed": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e" + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 40d440d83eb..6a6253f2e06 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -26,7 +26,7 @@ PLATFORMS = [ ] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Freedompro from a config entry.""" hass.data.setdefault(DOMAIN, {}) api_key = entry.data[CONF_API_KEY] @@ -43,7 +43,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.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/freedompro/binary_sensor.py b/homeassistant/components/freedompro/binary_sensor.py index 133f64019c2..ac70824be4c 100644 --- a/homeassistant/components/freedompro/binary_sensor.py +++ b/homeassistant/components/freedompro/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -47,14 +48,14 @@ class Device(CoordinatorEntity, BinarySensorEntity): self._attr_name = device["name"] self._attr_unique_id = device["uid"] self._type = device["type"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_device_class = DEVICE_CLASS_MAP[device["type"]] @callback diff --git a/homeassistant/components/freedompro/climate.py b/homeassistant/components/freedompro/climate.py index e37ae9dea1b..1707ee4a884 100644 --- a/homeassistant/components/freedompro/climate.py +++ b/homeassistant/components/freedompro/climate.py @@ -15,6 +15,7 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ATTR_TEMPERATURE, CONF_API_KEY, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -59,14 +60,14 @@ class Device(CoordinatorEntity, ClimateEntity): self._attr_name = device["name"] self._attr_unique_id = device["uid"] self._characteristics = device["characteristics"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE self._attr_current_temperature = 0 self._attr_target_temperature = 0 diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py index 439887c9626..fd6c747da46 100644 --- a/homeassistant/components/freedompro/cover.py +++ b/homeassistant/components/freedompro/cover.py @@ -18,6 +18,7 @@ from homeassistant.components.cover import ( from homeassistant.const import CONF_API_KEY from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -54,14 +55,14 @@ class Device(CoordinatorEntity, CoverEntity): self._api_key = api_key self._attr_name = device["name"] self._attr_unique_id = device["uid"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_current_cover_position = 0 self._attr_is_closed = True self._attr_supported_features = ( diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py index 55955042804..52c2de85ca6 100644 --- a/homeassistant/components/freedompro/fan.py +++ b/homeassistant/components/freedompro/fan.py @@ -7,6 +7,7 @@ from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.const import CONF_API_KEY from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -34,14 +35,14 @@ class FreedomproFan(CoordinatorEntity, FanEntity): self._attr_name = device["name"] self._attr_unique_id = device["uid"] self._characteristics = device["characteristics"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_is_on = False self._attr_percentage = 0 diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py index 0b944a682d4..5610a561fda 100644 --- a/homeassistant/components/freedompro/light.py +++ b/homeassistant/components/freedompro/light.py @@ -14,6 +14,7 @@ from homeassistant.components.light import ( from homeassistant.const import CONF_API_KEY from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -40,14 +41,14 @@ class Device(CoordinatorEntity, LightEntity): self._api_key = api_key self._attr_name = device["name"] self._attr_unique_id = device["uid"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_is_on = False self._attr_brightness = 0 color_mode = COLOR_MODE_ONOFF diff --git a/homeassistant/components/freedompro/lock.py b/homeassistant/components/freedompro/lock.py index f3a689016f6..57486f58d79 100644 --- a/homeassistant/components/freedompro/lock.py +++ b/homeassistant/components/freedompro/lock.py @@ -7,6 +7,7 @@ from homeassistant.components.lock import LockEntity from homeassistant.const import CONF_API_KEY from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -36,14 +37,14 @@ class Device(CoordinatorEntity, LockEntity): self._attr_unique_id = device["uid"] self._type = device["type"] self._characteristics = device["characteristics"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": self._type, - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=self._type, + name=self.name, + ) @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/freedompro/sensor.py b/homeassistant/components/freedompro/sensor.py index e5322924864..74b54474dbd 100644 --- a/homeassistant/components/freedompro/sensor.py +++ b/homeassistant/components/freedompro/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -54,14 +55,14 @@ class Device(CoordinatorEntity, SensorEntity): self._attr_name = device["name"] self._attr_unique_id = device["uid"] self._type = device["type"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_device_class = DEVICE_CLASS_MAP[device["type"]] self._attr_state_class = STATE_CLASS_MAP[device["type"]] self._attr_native_unit_of_measurement = UNIT_MAP[device["type"]] diff --git a/homeassistant/components/freedompro/switch.py b/homeassistant/components/freedompro/switch.py index c4c6b8ec353..c44af65ba32 100644 --- a/homeassistant/components/freedompro/switch.py +++ b/homeassistant/components/freedompro/switch.py @@ -7,6 +7,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_API_KEY from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -33,14 +34,14 @@ class Device(CoordinatorEntity, SwitchEntity): self._api_key = api_key self._attr_name = device["name"] self._attr_unique_id = device["uid"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_is_on = False @callback diff --git a/homeassistant/components/freedompro/translations/bg.json b/homeassistant/components/freedompro/translations/bg.json new file mode 100644 index 00000000000..1e5b299d96b --- /dev/null +++ b/homeassistant/components/freedompro/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "title": "Freedompro API \u043a\u043b\u044e\u0447" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index edde8c0c22a..994c7ff656e 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -25,16 +26,19 @@ SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( key="is_connected", name="Connection", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), BinarySensorEntityDescription( key="is_linked", name="Link", device_class=DEVICE_CLASS_PLUG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), BinarySensorEntityDescription( key="firmware_update", name="Firmware Update", device_class=DEVICE_CLASS_UPDATE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 61cff890a93..78b3f2073a7 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -17,15 +17,27 @@ from fritzconnection.core.exceptions import ( from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.device_tracker.const import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.components.switch import DOMAIN as DEVICE_SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + async_entries_for_config_entry, + async_get, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_registry import ( + EntityRegistry, + RegistryEntry, + async_entries_for_device, +) from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util @@ -35,6 +47,7 @@ from .const import ( DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, + SERVICE_CLEANUP, SERVICE_REBOOT, SERVICE_RECONNECT, TRACKER_SCAN_INTERVAL, @@ -70,6 +83,13 @@ def device_filter_out_from_trackers( return bool(reason) +def _cleanup_entity_filter(device: RegistryEntry) -> bool: + """Filter only relevant entities.""" + return device.domain == DEVICE_TRACKER_DOMAIN or ( + device.domain == DEVICE_SWITCH_DOMAIN and "_internet_access" in device.entity_id + ) + + class ClassSetupMissing(Exception): """Raised when a Class func is called before setup.""" @@ -281,29 +301,83 @@ class FritzBoxTools: _LOGGER.debug("Checking host info for FRITZ!Box router %s", self.host) self._update_available, self._latest_firmware = self._update_device_info() - async def service_fritzbox(self, service: str) -> None: + async def service_fritzbox( + self, service_call: ServiceCall, config_entry: ConfigEntry + ) -> None: """Define FRITZ!Box services.""" - _LOGGER.debug("FRITZ!Box router: %s", service) + _LOGGER.debug("FRITZ!Box router: %s", service_call.service) if not self.connection: raise HomeAssistantError("Unable to establish a connection") try: - if service == SERVICE_REBOOT: + if service_call.service == SERVICE_REBOOT: await self.hass.async_add_executor_job( self.connection.call_action, "DeviceConfig1", "Reboot" ) - elif service == SERVICE_RECONNECT: + return + + if service_call.service == SERVICE_RECONNECT: await self.hass.async_add_executor_job( self.connection.call_action, "WANIPConn1", "ForceTermination", ) + return + + if service_call.service == SERVICE_CLEANUP: + device_hosts_list: list = await self.hass.async_add_executor_job( + self.fritz_hosts.get_hosts_info + ) + except (FritzServiceError, FritzActionError) as ex: raise HomeAssistantError("Service or parameter unknown") from ex except FritzConnectionException as ex: raise HomeAssistantError("Service not supported") from ex + entity_reg: EntityRegistry = ( + await self.hass.helpers.entity_registry.async_get_registry() + ) + + ha_entity_reg_list: list[ + RegistryEntry + ] = self.hass.helpers.entity_registry.async_entries_for_config_entry( + entity_reg, config_entry.entry_id + ) + entities_removed: bool = False + + device_hosts_macs = {device["mac"] for device in device_hosts_list} + + for entry in ha_entity_reg_list: + if ( + not _cleanup_entity_filter(entry) + or entry.unique_id.split("_")[0] in device_hosts_macs + ): + continue + _LOGGER.info("Removing entity: %s", entry.name or entry.original_name) + entity_reg.async_remove(entry.entity_id) + entities_removed = True + + if entities_removed: + self._async_remove_empty_devices(entity_reg, config_entry) + + @callback + def _async_remove_empty_devices( + self, entity_reg: EntityRegistry, config_entry: ConfigEntry + ) -> None: + """Remove devices with no entities.""" + + device_reg = async_get(self.hass) + device_list = async_entries_for_config_entry(device_reg, config_entry.entry_id) + for device_entry in device_list: + if async_entries_for_device( + entity_reg, + device_entry.id, + include_disabled_entities=True, + ): + _LOGGER.info("Removing device: %s", device_entry.name) + device_reg.async_remove_device(device_entry.id) + @dataclass class FritzData: @@ -351,17 +425,17 @@ class FritzDeviceBase(Entity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - "identifiers": {(DOMAIN, self._mac)}, - "default_name": self.name, - "default_manufacturer": "AVM", - "default_model": "FRITZ!Box Tracked device", - "via_device": ( + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + default_manufacturer="AVM", + default_model="FRITZ!Box Tracked device", + default_name=self.name, + identifiers={(DOMAIN, self._mac)}, + via_device=( DOMAIN, self._router.unique_id, ), - } + ) @property def should_poll(self) -> bool: @@ -479,12 +553,12 @@ class FritzBoxBaseEntity: @property def device_info(self) -> DeviceInfo: """Return the device information.""" - - return { - "connections": {(CONNECTION_NETWORK_MAC, self.mac_address)}, - "identifiers": {(DOMAIN, self._fritzbox_tools.unique_id)}, - "name": self._device_name, - "manufacturer": "AVM", - "model": self._fritzbox_tools.model, - "sw_version": self._fritzbox_tools.current_firmware, - } + return DeviceInfo( + configuration_url=f"http://{self._fritzbox_tools.host}", + connections={(CONNECTION_NETWORK_MAC, self.mac_address)}, + identifiers={(DOMAIN, self._fritzbox_tools.unique_id)}, + manufacturer="AVM", + model=self._fritzbox_tools.model, + name=self._device_name, + sw_version=self._fritzbox_tools.current_firmware, + ) diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 3ed4e705730..d2c26d7bee8 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -22,6 +22,7 @@ ERROR_UNKNOWN = "unknown_error" FRITZ_SERVICES = "fritz_services" SERVICE_REBOOT = "reboot" SERVICE_RECONNECT = "reconnect" +SERVICE_CLEANUP = "cleanup" SWITCH_TYPE_DEFLECTION = "CallDeflection" SWITCH_TYPE_PORTFORWARD = "PortForward" diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 46531183afd..e6ae95e3eb4 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -3,7 +3,7 @@ "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ - "fritzconnection==1.6.0", + "fritzconnection==1.7.0", "xmltodict==0.12.0" ], "dependencies": ["network"], diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index fd82d245b9a..809af534f0e 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -27,6 +27,7 @@ from homeassistant.const import ( DATA_RATE_KILOBITS_PER_SECOND, DATA_RATE_KILOBYTES_PER_SECOND, DEVICE_CLASS_TIMESTAMP, + ENTITY_CATEGORY_DIAGNOSTIC, SIGNAL_STRENGTH_DECIBELS, ) from homeassistant.core import HomeAssistant @@ -165,12 +166,14 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( key="device_uptime", name="Device Uptime", device_class=DEVICE_CLASS_TIMESTAMP, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_device_uptime_state, ), FritzSensorEntityDescription( key="connection_uptime", name="Connection Uptime", device_class=DEVICE_CLASS_TIMESTAMP, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_connection_uptime_state, ), FritzSensorEntityDescription( @@ -194,6 +197,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( name="Max Connection Upload Throughput", native_unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:upload", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_max_kb_s_sent_state, ), FritzSensorEntityDescription( @@ -201,6 +205,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( name="Max Connection Download Throughput", native_unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:download", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_max_kb_s_received_state, ), FritzSensorEntityDescription( diff --git a/homeassistant/components/fritz/services.py b/homeassistant/components/fritz/services.py index 359e7ced239..2d8e16f15f0 100644 --- a/homeassistant/components/fritz/services.py +++ b/homeassistant/components/fritz/services.py @@ -5,15 +5,25 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.service import async_extract_config_entry_ids -from .const import DOMAIN, FRITZ_SERVICES, SERVICE_REBOOT, SERVICE_RECONNECT +from .common import FritzBoxTools +from .const import ( + DOMAIN, + FRITZ_SERVICES, + SERVICE_CLEANUP, + SERVICE_REBOOT, + SERVICE_RECONNECT, +) _LOGGER = logging.getLogger(__name__) +SERVICE_LIST = [SERVICE_CLEANUP, SERVICE_REBOOT, SERVICE_RECONNECT] + + async def async_setup_services(hass: HomeAssistant) -> None: """Set up services for Fritz integration.""" - for service in (SERVICE_REBOOT, SERVICE_RECONNECT): + for service in SERVICE_LIST: if hass.services.has_service(DOMAIN, service): return @@ -29,12 +39,18 @@ async def async_setup_services(hass: HomeAssistant) -> None: f"Failed to call service '{service_call.service}'. Config entry for target not found" ) - for entry in fritzbox_entry_ids: + for entry_id in fritzbox_entry_ids: _LOGGER.debug("Executing service %s", service_call.service) - fritz_tools = hass.data[DOMAIN][entry] - await fritz_tools.service_fritzbox(service_call.service) + fritz_tools: FritzBoxTools = hass.data[DOMAIN][entry_id] + if config_entry := hass.config_entries.async_get_entry(entry_id): + await fritz_tools.service_fritzbox(service_call, config_entry) + else: + _LOGGER.error( + "Executing service %s failed, no config entry found", + service_call.service, + ) - for service in (SERVICE_REBOOT, SERVICE_RECONNECT): + for service in SERVICE_LIST: hass.services.async_register(DOMAIN, service, async_call_fritz_service) @@ -59,5 +75,5 @@ async def async_unload_services(hass: HomeAssistant) -> None: hass.data[FRITZ_SERVICES] = False - hass.services.async_remove(DOMAIN, SERVICE_REBOOT) - hass.services.async_remove(DOMAIN, SERVICE_RECONNECT) + for service in SERVICE_LIST: + hass.services.async_remove(DOMAIN, service) diff --git a/homeassistant/components/fritz/services.yaml b/homeassistant/components/fritz/services.yaml index 87b0e6fca71..2375aa71f57 100644 --- a/homeassistant/components/fritz/services.yaml +++ b/homeassistant/components/fritz/services.yaml @@ -1,13 +1,37 @@ reconnect: description: Reconnects your FRITZ!Box internet connection - target: - entity: - integration: fritz - domain: binary_sensor - + fields: + device_id: + name: Fritz!Box Device + description: Select the Fritz!Box to reconnect + required: true + selector: + device: + integration: fritz + entity: + device_class: connectivity reboot: description: Reboots your FRITZ!Box - target: - entity: - integration: fritz - domain: binary_sensor + fields: + device_id: + name: Fritz!Box Device + description: Select the Fritz!Box to reboot + required: true + selector: + device: + integration: fritz + entity: + device_class: connectivity + +cleanup: + description: Remove FRITZ!Box stale device_tracker entities + fields: + device_id: + name: Fritz!Box Device + description: Select the Fritz!Box to check + required: true + selector: + device: + integration: fritz + entity: + device_class: connectivity diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index a53d0867a3c..969f8cf8f9e 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -18,6 +18,7 @@ import xmltodict from homeassistant.components.network import async_get_source_ip from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -441,6 +442,7 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch, SwitchEntity): self.connection_type = connection_type self.port_mapping = port_mapping # dict in the format as it comes from fritzconnection. eg: {'NewRemoteHost': '0.0.0.0', 'NewExternalPort': 22, 'NewProtocol': 'TCP', 'NewInternalPort': 22, 'NewInternalClient': '192.168.178.31', 'NewEnabled': True, 'NewPortMappingDescription': 'Beast SSH ', 'NewLeaseDuration': 0} self._idx = idx # needed for update routine + self._attr_entity_category = ENTITY_CATEGORY_CONFIG if port_mapping is None: return @@ -519,6 +521,7 @@ class FritzBoxDeflectionSwitch(FritzBoxBaseSwitch, SwitchEntity): self.dict_of_deflection = dict_of_deflection self._attributes = {} self.id = int(self.dict_of_deflection["DeflectionId"]) + self._attr_entity_category = ENTITY_CATEGORY_CONFIG switch_info = SwitchInfo( description=f"Call deflection {self.id}", @@ -588,6 +591,7 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity): self._attr_is_on: bool = False self._name = f"{device.hostname} Internet Access" self._attr_unique_id = f"{self._mac}_internet_access" + self._attr_entity_category = ENTITY_CATEGORY_CONFIG async def async_process_update(self) -> None: """Update device.""" @@ -648,6 +652,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch, SwitchEntity): self._fritzbox_tools = fritzbox_tools self._attributes = {} + self._attr_entity_category = ENTITY_CATEGORY_CONFIG self._network_num = network_num switch_info = SwitchInfo( diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json new file mode 100644 index 00000000000..b1ea395f077 --- /dev/null +++ b/homeassistant/components/fritz/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "start_config": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 8d354f655f6..e72e1d86fc1 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,10 +1,7 @@ """Support for AVM FRITZ!SmartHome devices.""" from __future__ import annotations -from datetime import timedelta - from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError -import requests from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -18,10 +15,7 @@ from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_STATE_DEVICE_LOCKED, @@ -32,6 +26,7 @@ from .const import ( LOGGER, PLATFORMS, ) +from .coordinator import FritzboxDataUpdateCoordinator from .model import FritzExtraAttributes @@ -53,52 +48,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CONF_CONNECTIONS: fritz, } - def _update_fritz_devices() -> dict[str, FritzhomeDevice]: - """Update all fritzbox device data.""" - try: - devices = fritz.get_devices() - except requests.exceptions.HTTPError: - # If the device rebooted, login again - try: - fritz.login() - except requests.exceptions.HTTPError as ex: - raise ConfigEntryAuthFailed from ex - devices = fritz.get_devices() - - data = {} - for device in devices: - device.update() - - # assume device as unavailable, see #55799 - if ( - device.has_powermeter - and device.present - and hasattr(device, "voltage") - and device.voltage <= 0 - and device.power <= 0 - and device.energy <= 0 - ): - device.present = False - - data[device.ain] = device - return data - - async def async_update_coordinator() -> dict[str, FritzhomeDevice]: - """Fetch all device data.""" - return await hass.async_add_executor_job(_update_fritz_devices) - - hass.data[DOMAIN][entry.entry_id][ - CONF_COORDINATOR - ] = coordinator = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{entry.entry_id}", - update_method=async_update_coordinator, - update_interval=timedelta(seconds=30), - ) + coordinator = FritzboxDataUpdateCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id][CONF_COORDINATOR] = coordinator + def _update_unique_id(entry: RegistryEntry) -> dict[str, str] | None: """Update unique ID of entity entry.""" if ( @@ -142,9 +97,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class FritzBoxEntity(CoordinatorEntity): """Basis FritzBox entity.""" + coordinator: FritzboxDataUpdateCoordinator + def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + coordinator: FritzboxDataUpdateCoordinator, ain: str, entity_description: EntityDescription | None = None, ) -> None: @@ -173,13 +130,14 @@ class FritzBoxEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "name": self.device.name, - "identifiers": {(DOMAIN, self.ain)}, - "manufacturer": self.device.manufacturer, - "model": self.device.productname, - "sw_version": self.device.fw_version, - } + return DeviceInfo( + name=self.device.name, + identifiers={(DOMAIN, self.ain)}, + manufacturer=self.device.manufacturer, + model=self.device.productname, + sw_version=self.device.fw_version, + configuration_url=self.coordinator.configuration_url, + ) @property def extra_state_attributes(self) -> FritzExtraAttributes: diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 1317710c570..b0f5e63d424 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -15,10 +15,10 @@ 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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import FritzBoxEntity from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN +from .coordinator import FritzboxDataUpdateCoordinator from .model import FritzEntityDescriptionMixinBase @@ -70,7 +70,7 @@ class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + coordinator: FritzboxDataUpdateCoordinator, ain: str, entity_description: FritzBinarySensorEntityDescription, ) -> None: diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 3ae3368f4ae..bcf17a1a958 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -125,8 +125,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): assert isinstance(host, str) self.context[CONF_HOST] = host - uuid = discovery_info.get(ATTR_UPNP_UDN) - if uuid: + if uuid := discovery_info.get(ATTR_UPNP_UDN): if uuid.startswith("uuid:"): uuid = uuid[5:] await self.async_set_unique_id(uuid) diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 67e7c9dc564..9d537bec617 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -11,6 +11,9 @@ ATTR_STATE_LOCKED: Final = "locked" ATTR_STATE_SUMMER_MODE: Final = "summer_mode" ATTR_STATE_WINDOW_OPEN: Final = "window_open" +COLOR_MODE: Final = "1" +COLOR_TEMP_MODE: Final = "4" + CONF_CONNECTIONS: Final = "connections" CONF_COORDINATOR: Final = "coordinator" @@ -21,4 +24,4 @@ DOMAIN: Final = "fritzbox" LOGGER: Final[logging.Logger] = logging.getLogger(__package__) -PLATFORMS: Final[list[str]] = ["binary_sensor", "climate", "switch", "sensor"] +PLATFORMS: Final[list[str]] = ["binary_sensor", "climate", "light", "switch", "sensor"] diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py new file mode 100644 index 00000000000..69ab0b4c274 --- /dev/null +++ b/homeassistant/components/fritzbox/coordinator.py @@ -0,0 +1,68 @@ +"""Data update coordinator for AVM FRITZ!SmartHome devices.""" +from __future__ import annotations + +from datetime import timedelta + +from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError +import requests + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_CONNECTIONS, DOMAIN, LOGGER + + +class FritzboxDataUpdateCoordinator(DataUpdateCoordinator): + """Fritzbox Smarthome device data update coordinator.""" + + configuration_url: str + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the Fritzbox Smarthome device coordinator.""" + self.entry = entry + self.fritz: Fritzhome = hass.data[DOMAIN][self.entry.entry_id][CONF_CONNECTIONS] + self.configuration_url = self.fritz.get_prefixed_host() + super().__init__( + hass, + LOGGER, + name=entry.entry_id, + update_interval=timedelta(seconds=30), + ) + + def _update_fritz_devices(self) -> dict[str, FritzhomeDevice]: + """Update all fritzbox device data.""" + try: + devices = self.fritz.get_devices() + except requests.exceptions.ConnectionError as ex: + raise ConfigEntryNotReady from ex + except requests.exceptions.HTTPError: + # If the device rebooted, login again + try: + self.fritz.login() + except LoginError as ex: + raise ConfigEntryAuthFailed from ex + devices = self.fritz.get_devices() + + data = {} + self.fritz.update_devices() + for device in devices: + # assume device as unavailable, see #55799 + if ( + device.has_powermeter + and device.present + and hasattr(device, "voltage") + and device.voltage <= 0 + and device.power <= 0 + and device.energy <= 0 + ): + LOGGER.debug("Assume device %s as unavailable", device.name) + device.present = False + + data[device.ain] = device + return data + + async def _async_update_data(self) -> dict[str, FritzhomeDevice]: + """Fetch all device data.""" + return await self.hass.async_add_executor_job(self._update_fritz_devices) diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py new file mode 100644 index 00000000000..272d170e13d --- /dev/null +++ b/homeassistant/components/fritzbox/light.py @@ -0,0 +1,153 @@ +"""Support for AVM FRITZ!SmartHome lightbulbs.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import color + +from . import FritzBoxEntity +from .const import ( + COLOR_MODE, + COLOR_TEMP_MODE, + CONF_COORDINATOR, + DOMAIN as FRITZBOX_DOMAIN, +) +from .coordinator import FritzboxDataUpdateCoordinator + +SUPPORTED_COLOR_MODES = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the FRITZ!SmartHome light from ConfigEntry.""" + entities: list[FritzboxLight] = [] + coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] + + for ain, device in coordinator.data.items(): + if not device.has_lightbulb: + continue + + supported_color_temps = await hass.async_add_executor_job( + device.get_color_temps + ) + + supported_colors = await hass.async_add_executor_job(device.get_colors) + + entities.append( + FritzboxLight( + coordinator, + ain, + supported_colors, + supported_color_temps, + ) + ) + + async_add_entities(entities) + + +class FritzboxLight(FritzBoxEntity, LightEntity): + """The light class for FRITZ!SmartHome lightbulbs.""" + + def __init__( + self, + coordinator: FritzboxDataUpdateCoordinator, + ain: str, + supported_colors: dict, + supported_color_temps: list[str], + ) -> None: + """Initialize the FritzboxLight entity.""" + super().__init__(coordinator, ain, None) + + max_kelvin = int(max(supported_color_temps)) + min_kelvin = int(min(supported_color_temps)) + + # max kelvin is min mireds and min kelvin is max mireds + self._attr_min_mireds = color.color_temperature_kelvin_to_mired(max_kelvin) + self._attr_max_mireds = color.color_temperature_kelvin_to_mired(min_kelvin) + + # Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each. + # Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup + self._supported_hs = {} + for values in supported_colors.values(): + hue = int(values[0][0]) + self._supported_hs[hue] = [ + int(values[0][1]), + int(values[1][1]), + int(values[2][1]), + ] + + @property + def is_on(self) -> bool: + """If the light is currently on or off.""" + return self.device.state # type: ignore [no-any-return] + + @property + def brightness(self) -> int: + """Return the current Brightness.""" + return self.device.level # type: ignore [no-any-return] + + @property + def hs_color(self) -> tuple[float, float] | None: + """Return the hs color value.""" + if self.device.color_mode != COLOR_MODE: + return None + + hue = self.device.hue + saturation = self.device.saturation + + return (hue, float(saturation) * 100.0 / 255.0) + + @property + def color_temp(self) -> int | None: + """Return the CT color value.""" + if self.device.color_mode != COLOR_TEMP_MODE: + return None + + kelvin = self.device.color_temp + return color.color_temperature_kelvin_to_mired(kelvin) + + @property + def supported_color_modes(self) -> set: + """Flag supported color modes.""" + return SUPPORTED_COLOR_MODES + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + if kwargs.get(ATTR_BRIGHTNESS) is not None: + level = kwargs[ATTR_BRIGHTNESS] + await self.hass.async_add_executor_job(self.device.set_level, level) + if kwargs.get(ATTR_HS_COLOR) is not None: + hass_hue = int(kwargs[ATTR_HS_COLOR][0]) + hass_saturation = round(kwargs[ATTR_HS_COLOR][1] * 255.0 / 100.0) + # find supported hs values closest to what user selected + hue = min(self._supported_hs.keys(), key=lambda x: abs(x - hass_hue)) + saturation = min( + self._supported_hs[hue], key=lambda x: abs(x - hass_saturation) + ) + await self.hass.async_add_executor_job( + self.device.set_color, (hue, saturation) + ) + + if kwargs.get(ATTR_COLOR_TEMP) is not None: + kelvin = color.color_temperature_kelvin_to_mired(kwargs[ATTR_COLOR_TEMP]) + await self.hass.async_add_executor_job(self.device.set_color_temp, kelvin) + + await self.hass.async_add_executor_job(self.device.set_state_on) + await self.coordinator.async_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self.hass.async_add_executor_job(self.device.set_state_off) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index c1db226d348..98c02d0166e 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -8,7 +8,7 @@ "st": "urn:schemas-upnp-org:device:fritzbox:1" } ], - "codeowners": ["@mib1185"], + "codeowners": ["@mib1185", "@flabbamann"], "config_flow": true, "iot_class": "local_polling" } diff --git a/homeassistant/components/fritzbox/translations/bg.json b/homeassistant/components/fritzbox/translations/bg.json index ec678d2d76c..ac7e60b9afc 100644 --- a/homeassistant/components/fritzbox/translations/bg.json +++ b/homeassistant/components/fritzbox/translations/bg.json @@ -1,11 +1,29 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "flow_title": "{name}", "step": { + "confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } } } } diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json index c5d5e495131..c1cf8154aea 100644 --- a/homeassistant/components/fritzbox/translations/hu.json +++ b/homeassistant/components/fritzbox/translations/hu.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "not_supported": "Csatlakoztatva az AVM FRITZ! Boxhoz, de nem tudja vez\u00e9relni az intelligens otthoni eszk\u00f6z\u00f6ket.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" @@ -24,7 +24,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Friss\u00edtse a(z) {name} bejelentkez\u00e9si adatait." + "description": "Friss\u00edtse {name} bejelentkez\u00e9si adatait." }, "user": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 0a1f7330c6d..3d58f27e950 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "AVM FRITZ!Box Call Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==1.6.0"], + "requirements": ["fritzconnection==1.7.0"], "codeowners": [], "iot_class": "local_polling" } diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 31e04077656..171e6966b28 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from .const import ( ATTR_PREFIXES, @@ -175,15 +176,15 @@ class FritzBoxCallSensor(SensorEntity): return self._attributes @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "name": self._fritzbox_phonebook.fph.modelname, - "identifiers": {(DOMAIN, self._unique_id)}, - "manufacturer": MANUFACTURER, - "model": self._fritzbox_phonebook.fph.modelname, - "sw_version": self._fritzbox_phonebook.fph.fc.system_version, - } + return DeviceInfo( + identifiers={(DOMAIN, self._unique_id)}, + manufacturer=MANUFACTURER, + model=self._fritzbox_phonebook.fph.modelname, + name=self._fritzbox_phonebook.fph.modelname, + sw_version=self._fritzbox_phonebook.fph.fc.system_version, + ) @property def unique_id(self): diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 4c21e83e191..e40b5303eca 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.6.0"], + "requirements": ["pyfronius==0.7.0"], "codeowners": ["@nielstron"], "iot_class": "local_polling" } diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 076eee9acc8..0ad172c2ab0 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -6,7 +6,7 @@ from datetime import timedelta import logging from typing import Any -from pyfronius import Fronius +from pyfronius import Fronius, FroniusError import voluptuous as vol from homeassistant.components.sensor import ( @@ -205,19 +205,13 @@ class FroniusAdapter: """Retrieve and update latest state.""" try: values = await self._update() - except ConnectionError: + except FroniusError as err: # fronius devices are often powered by self-produced solar energy # and henced turned off at night. # Therefore we will not print multiple errors when connection fails if self._available: self._available = False - _LOGGER.error("Failed to update: connection error") - return - except ValueError: - _LOGGER.error( - "Failed to update: invalid response returned." - "Maybe the configured device is not supported" - ) + _LOGGER.error("Failed to update: %s", err) return self._available = True # reset connection failure diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8b92745f4d4..005384b6beb 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -567,8 +567,7 @@ class IndexView(web_urldispatcher.AbstractResource): def get_template(self) -> jinja2.Template: """Get template.""" - tpl = self._template_cache - if tpl is None: + if (tpl := self._template_cache) is None: with (_frontend_root(self.repo_path) / "index.html").open( encoding="utf8" ) as file: diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 5b908efb2df..c913de3367f 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==20211007.1" + "home-assistant-frontend==20211103.0" ], "dependencies": [ "api", diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index 0b04655bd86..d7aabbec9b8 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -33,9 +33,8 @@ def with_store(orig_func: Callable) -> Callable: """Provide user specific data and store to function.""" stores, data = hass.data[DATA_STORAGE] user_id = connection.user.id - store = stores.get(user_id) - if store is None: + if (store := stores.get(user_id)) is None: store = stores[user_id] = hass.helpers.storage.Store( STORAGE_VERSION_USER_DATA, f"frontend.user_data_{connection.user.id}" ) diff --git a/homeassistant/components/garages_amsterdam/translations/bg.json b/homeassistant/components/garages_amsterdam/translations/bg.json new file mode 100644 index 00000000000..3348117ce6b --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/bg.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index 7e3dc7484bb..fb0782de525 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -138,8 +138,7 @@ class GdacsEvent(GeolocationEvent): def _update_from_feed(self, feed_entry): """Update the internal state from the provided feed entry.""" - event_name = feed_entry.event_name - if not event_name: + if not (event_name := feed_entry.event_name): # Earthquakes usually don't have an event name. event_name = f"{feed_entry.country} ({feed_entry.event_id})" self._title = f"{feed_entry.event_type}: {event_name}" diff --git a/homeassistant/components/generic_hygrostat/__init__.py b/homeassistant/components/generic_hygrostat/__init__.py index 568863adb73..b58c98b1d0d 100644 --- a/homeassistant/components/generic_hygrostat/__init__.py +++ b/homeassistant/components/generic_hygrostat/__init__.py @@ -1,7 +1,5 @@ """The generic_hygrostat component.""" -import logging - import voluptuous as vol from homeassistant.components.humidifier.const import ( @@ -13,8 +11,6 @@ from homeassistant.helpers import config_validation as cv, discovery DOMAIN = "generic_hygrostat" -_LOGGER = logging.getLogger(__name__) - CONF_HUMIDIFIER = "humidifier" CONF_SENSOR = "target_sensor" CONF_MIN_HUMIDITY = "min_humidity" diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index ee1c8f65d1a..726b6e654e7 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -204,14 +204,11 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): return self._active @property - def state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" - data = super().state_attributes - if self._saved_target_humidity: - data[ATTR_SAVED_HUMIDITY] = self._saved_target_humidity - - return data + return {ATTR_SAVED_HUMIDITY: self._saved_target_humidity} + return None @property def should_poll(self): diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index c48deba12d8..4d52240535f 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -364,8 +364,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self._target_temp = temperature await self._async_control_heating(force=True) diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index b77aecee14c..c030b3d3075 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -52,8 +52,7 @@ async def async_attach_trigger(hass, config, action, automation_info): if not source_match(from_state, source) and not source_match(to_state, source): return - zone_state = hass.states.get(zone_entity_id) - if zone_state is None: + if (zone_state := hass.states.get(zone_entity_id)) is None: _LOGGER.warning( "Unable to execute automation %s: Zone %s not found", automation_info["name"], diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 6d604ec6e30..1e8e3eb1f04 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -1,4 +1,6 @@ """Support for Geofency.""" +from http import HTTPStatus + from aiohttp import web import voluptuous as vol @@ -8,7 +10,6 @@ from homeassistant.const import ( ATTR_LONGITUDE, ATTR_NAME, CONF_WEBHOOK_ID, - HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME, ) from homeassistant.helpers import config_entry_flow @@ -88,7 +89,9 @@ async def handle_webhook(hass, webhook_id, request): try: data = WEBHOOK_SCHEMA(dict(await request.post())) except vol.MultipleInvalid as error: - return web.Response(text=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY) + return web.Response( + text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY + ) if _is_mobile_beacon(data, hass.data[DOMAIN]["beacons"]): return _set_location(hass, data, None) diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 5a58e73d44a..b2d26dcb2a5 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -5,6 +5,7 @@ from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.restore_state import RestoreEntity from . import DOMAIN as GF_DOMAIN, TRACKER_UPDATE @@ -86,9 +87,9 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return {"name": self._name, "identifiers": {(GF_DOMAIN, self._unique_id)}} + return DeviceInfo(identifiers={(GF_DOMAIN, self._unique_id)}, name=self._name) @property def source_type(self): diff --git a/homeassistant/components/geofency/translations/bg.json b/homeassistant/components/geofency/translations/bg.json index de2e8af5d97..916336f37a4 100644 --- a/homeassistant/components/geofency/translations/bg.json +++ b/homeassistant/components/geofency/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 Geofency. \n\n \u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." }, diff --git a/homeassistant/components/geofency/translations/hu.json b/homeassistant/components/geofency/translations/hu.json index de8f368adb3..1b3f17fe700 100644 --- a/homeassistant/components/geofency/translations/hu.json +++ b/homeassistant/components/geofency/translations/hu.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." + "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtano a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1lja: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_quakes/translations/cs.json b/homeassistant/components/geonetnz_quakes/translations/cs.json index 0ddca983798..3613280d3e5 100644 --- a/homeassistant/components/geonetnz_quakes/translations/cs.json +++ b/homeassistant/components/geonetnz_quakes/translations/cs.json @@ -6,8 +6,10 @@ "step": { "user": { "data": { + "mmi": "MMI", "radius": "Polom\u011br" - } + }, + "title": "Vypl\u0148te \u00fadaje filtru." } } } diff --git a/homeassistant/components/geonetnz_quakes/translations/he.json b/homeassistant/components/geonetnz_quakes/translations/he.json index 48a6eeeea33..7718605a588 100644 --- a/homeassistant/components/geonetnz_quakes/translations/he.json +++ b/homeassistant/components/geonetnz_quakes/translations/he.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "step": { + "user": { + "data": { + "radius": "\u05e8\u05d3\u05d9\u05d5\u05e1" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/translations/hu.json b/homeassistant/components/geonetnz_quakes/translations/hu.json index d6070db4fe7..c67444ec274 100644 --- a/homeassistant/components/geonetnz_quakes/translations/hu.json +++ b/homeassistant/components/geonetnz_quakes/translations/hu.json @@ -9,7 +9,7 @@ "mmi": "MMI", "radius": "Sug\u00e1r" }, - "title": "T\u00f6ltsd ki a sz\u0171r\u0151 adatait." + "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait." } } } diff --git a/homeassistant/components/geonetnz_volcano/translations/bg.json b/homeassistant/components/geonetnz_volcano/translations/bg.json index 042696219fc..17f38fa0971 100644 --- a/homeassistant/components/geonetnz_volcano/translations/bg.json +++ b/homeassistant/components/geonetnz_volcano/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 4f19b0d8a68..9b98b0bda26 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -27,6 +27,8 @@ SCAN_INTERVAL: Final = timedelta(minutes=30) DOMAIN: Final = "gios" MANUFACTURER: Final = "Główny Inspektorat Ochrony Środowiska" +URL = "http://powietrze.gios.gov.pl/pjp/current/station_details/info/{station_id}" + API_TIMEOUT: Final = 30 ATTR_INDEX: Final = "index" diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 3e7bf9aceca..0e7227797d2 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==2.0.0"], + "requirements": ["gios==2.1.0"], "config_flow": true, "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 9ba5e5410b0..f60a8e99d5a 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import DOMAIN as PLATFORM, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, ATTR_NAME, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.typing import StateType @@ -24,6 +25,7 @@ from .const import ( DOMAIN, MANUFACTURER, SENSOR_TYPES, + URL, ) from .model import GiosSensorEntityDescription @@ -80,12 +82,13 @@ class GiosSensor(CoordinatorEntity, SensorEntity): ) -> None: """Initialize.""" super().__init__(coordinator) - self._attr_device_info = { - "identifiers": {(DOMAIN, str(coordinator.gios.station_id))}, - "name": DEFAULT_NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, str(coordinator.gios.station_id))}, + manufacturer=MANUFACTURER, + name=DEFAULT_NAME, + configuration_url=URL.format(station_id=coordinator.gios.station_id), + ) self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{coordinator.gios.station_id}-{description.key}" self._attrs: dict[str, Any] = { diff --git a/homeassistant/components/gios/translations/bg.json b/homeassistant/components/gios/translations/bg.json new file mode 100644 index 00000000000..35cfa0ad1d7 --- /dev/null +++ b/homeassistant/components/gios/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/zh-Hant.json b/homeassistant/components/gios/translations/zh-Hant.json index d72bc9bc015..98a62385ee1 100644 --- a/homeassistant/components/gios/translations/zh-Hant.json +++ b/homeassistant/components/gios/translations/zh-Hant.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_sensors_data": "\u6b64\u76e3\u6e2c\u7ad9\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6548\u3002", + "invalid_sensors_data": "\u6b64\u76e3\u6e2c\u7ad9\u611f\u6e2c\u5668\u8cc7\u6599\u7121\u6548\u3002", "wrong_station_id": "\u76e3\u6e2c\u7ad9 ID \u4e0d\u6b63\u78ba\u3002" }, "step": { diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 92173f9d143..b0960b3531a 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -113,8 +113,7 @@ class GlancesSensor(SensorEntity): async def async_update(self): # noqa: C901 """Get the latest data from REST API.""" - value = self.glances_data.api.data - if value is None: + if (value := self.glances_data.api.data) is None: return if self.entity_description.type == "fs": diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 7a179c46210..774f1fd0e21 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -9,16 +9,7 @@ from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSO from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, - CONF_HOST, - CONF_NAME, -) +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_MODEL, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -54,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except exceptions.ConnectError as ex: raise ConfigEntryNotReady(f"Failed to connect to device: {ex}") from ex - async def async_update_data(): + async def async_update_data() -> None: """Fetch data from API endpoint.""" try: await api.get_state() @@ -68,6 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_data, update_interval=MIN_TIME_BETWEEN_UPDATES, ) + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_KEY_API: api, @@ -108,10 +100,10 @@ class YetiEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return the device information of the entity.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._server_unique_id)}, - ATTR_MANUFACTURER: "Goal Zero", - ATTR_NAME: self._name, - ATTR_MODEL: self.api.sysdata.get(ATTR_MODEL), - ATTR_SW_VERSION: self.api.data.get("firmwareVersion"), - } + return DeviceInfo( + identifiers={(DOMAIN, self._server_unique_id)}, + manufacturer="Goal Zero", + model=self.api.sysdata[ATTR_MODEL], + name=self._name, + sw_version=self.api.data["firmwareVersion"], + ) diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index 21eecc678ad..56bbe1e2261 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Goal Zero Yeti Sensors.""" from __future__ import annotations +from typing import cast + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_CONNECTIVITY, @@ -9,7 +11,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -29,6 +31,7 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( key="app_online", name="App Online", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), BinarySensorEntityDescription( key="isCharging", @@ -80,5 +83,5 @@ class YetiBinarySensor(YetiEntity, BinarySensorEntity): @property def is_on(self) -> bool: - """Return if the service is on.""" - return self.api.data.get(self.entity_description.key) == 1 + """Return True if the service is on.""" + return cast(bool, self.api.data[self.entity_description.key] == 1) diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index 957891e67ed..6677936b35a 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -1,6 +1,8 @@ """Support for Goal Zero Yeti Sensors.""" from __future__ import annotations +from typing import cast + from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, @@ -20,6 +22,7 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, POWER_WATT, SIGNAL_STRENGTH_DECIBELS, @@ -29,6 +32,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import Yeti, YetiEntity @@ -104,6 +108,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key="wifiStrength", @@ -111,22 +116,26 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key="timestamp", name="Total Run Time", native_unit_of_measurement=TIME_SECONDS, entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key="ssid", name="Wi-Fi SSID", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key="ipAddr", name="IP Address", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) @@ -168,6 +177,6 @@ class YetiSensor(YetiEntity, SensorEntity): self._attr_unique_id = f"{server_unique_id}/{description.key}" @property - def native_value(self) -> str: + def native_value(self) -> StateType: """Return the state.""" - return self.api.data.get(self.entity_description.key) + return cast(StateType, self.api.data[self.entity_description.key]) diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 767c728e62b..6c80a773a74 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -1,6 +1,8 @@ """Support for Goal Zero Yeti Switches.""" from __future__ import annotations +from typing import Any, cast + from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME @@ -65,15 +67,15 @@ class YetiSwitch(YetiEntity, SwitchEntity): @property def is_on(self) -> bool: """Return state of the switch.""" - return self.api.data.get(self.entity_description.key) + return cast(bool, self.api.data[self.entity_description.key] == 1) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" payload = {self.entity_description.key: 0} await self.api.post_state(payload=payload) self.coordinator.async_set_updated_data(data=payload) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" payload = {self.entity_description.key: 1} await self.api.post_state(payload=payload) diff --git a/homeassistant/components/goalzero/translations/bg.json b/homeassistant/components/goalzero/translations/bg.json new file mode 100644 index 00000000000..2461fa173ae --- /dev/null +++ b/homeassistant/components/goalzero/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 5d190034028..a70a1b6bf81 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -92,16 +93,20 @@ class GoGoGate2Entity(CoordinatorEntity): return self._door @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for the controller.""" data = self.coordinator.data - return { - "identifiers": {(DOMAIN, self._config_entry.unique_id)}, - "name": self._config_entry.title, - "manufacturer": MANUFACTURER, - "model": data.model, - "sw_version": data.firmwareversion, - } + configuration_url = ( + f"https://{data.remoteaccess}" if data.remoteaccess else None + ) + return DeviceInfo( + configuration_url=configuration_url, + identifiers={(DOMAIN, str(self._config_entry.unique_id))}, + name=self._config_entry.title, + manufacturer=MANUFACTURER, + model=data.model, + sw_version=data.firmwareversion, + ) def get_data_update_coordinator( diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 073c48e55b8..fb5871e8636 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -1,8 +1,6 @@ """Support for Gogogate2 garage Doors.""" from __future__ import annotations -import logging - from ismartgate.common import ( AbstractDoor, DoorStatus, @@ -28,8 +26,6 @@ from .common import ( get_data_update_coordinator, ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index 94a57c47be7..90d50bdda43 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -3,7 +3,7 @@ "name": "Gogogate2 and ismartgate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", - "requirements": ["ismartgate==4.0.0"], + "requirements": ["ismartgate==4.0.4"], "codeowners": ["@vangorra", "@bdraco"], "homekit": { "models": ["iSmartGate"] diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index 7ad248b88d6..6eb3d823c22 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -5,11 +5,12 @@ from itertools import chain from ismartgate.common import AbstractDoor, get_configured_doors -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -51,6 +52,8 @@ async def async_setup_entry( class DoorSensorBattery(GoGoGate2Entity, SensorEntity): """Battery sensor entity for gogogate2 door sensor.""" + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + def __init__( self, config_entry: ConfigEntry, @@ -77,6 +80,11 @@ class DoorSensorBattery(GoGoGate2Entity, SensorEntity): door = self._get_door() return door.voltage # This is a percentage, not an absolute voltage + @property + def state_class(self) -> str: + """Return the Measurement State Class.""" + return STATE_CLASS_MEASUREMENT + @property def extra_state_attributes(self): """Return the state attributes.""" @@ -104,6 +112,11 @@ class DoorSensorTemperature(GoGoGate2Entity, SensorEntity): """Return the name of the door.""" return f"{self._get_door().name} temperature" + @property + def state_class(self) -> str: + """Return the Measurement State Class.""" + return STATE_CLASS_MEASUREMENT + @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 7e157d238d5..08663a297d2 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -233,8 +233,7 @@ def setup(hass, config): if DATA_INDEX not in hass.data: hass.data[DATA_INDEX] = {} - conf = config.get(DOMAIN, {}) - if not conf: + if not (conf := config.get(DOMAIN, {})): # component is set up by tts platform return True diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 4e3ade38e39..14667dbb303 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from asyncio import gather from collections.abc import Mapping +from http import HTTPStatus import logging import pprint @@ -53,8 +54,7 @@ async def _get_entity_and_device( hass.helpers.entity_registry.async_get_registry(), ) - entity_entry = ent_reg.async_get(entity_id) - if not entity_entry: + if not (entity_entry := ent_reg.async_get(entity_id)): return None, None device_entry = dev_reg.devices.get(entity_entry.device_id) return entity_entry, device_entry @@ -204,7 +204,7 @@ class AbstractConfig(ABC): # Remove any pending sync self._google_sync_unsub.pop(agent_user_id, lambda: None)() status = await self._async_request_sync_devices(agent_user_id) - if status == 404: + if status == HTTPStatus.NOT_FOUND: await self.async_disconnect_agent_user(agent_user_id) return status @@ -263,9 +263,7 @@ class AbstractConfig(ABC): @callback def async_enable_local_sdk(self): """Enable the local SDK.""" - webhook_id = self.local_sdk_webhook_id - - if webhook_id is None: + if (webhook_id := self.local_sdk_webhook_id) is None: return try: @@ -500,8 +498,7 @@ class GoogleEntity: } # use aliases - aliases = entity_config.get(CONF_ALIASES) - if aliases: + if aliases := entity_config.get(CONF_ALIASES): device["name"]["nicknames"] = [name] + aliases if self.config.is_local_sdk_active and self.should_expose_local(): @@ -518,8 +515,7 @@ class GoogleEntity: for trt in traits: device["attributes"].update(trt.sync_attributes()) - room = entity_config.get(CONF_ROOM_HINT) - if room: + if room := entity_config.get(CONF_ROOM_HINT): device["roomHint"] = room else: area = await _get_area(self.hass, entity_entry, device_entry) diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 61768ff2be8..ba7dc2597bc 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -1,6 +1,7 @@ """Support for Google Actions Smart Home Control.""" import asyncio from datetime import timedelta +from http import HTTPStatus import logging from uuid import uuid4 @@ -12,9 +13,10 @@ import jwt from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( CLOUD_NEVER_EXPOSED_ENTITIES, - HTTP_INTERNAL_SERVER_ERROR, - HTTP_UNAUTHORIZED, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, ) +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util @@ -112,16 +114,30 @@ class GoogleConfig(AbstractConfig): if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: return False + entity_registry = er.async_get(self.hass) + registry_entry = entity_registry.async_get(state.entity_id) + if registry_entry: + auxiliary_entity = registry_entry.entity_category in ( + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ) + else: + auxiliary_entity = False + explicit_expose = self.entity_config.get(state.entity_id, {}).get(CONF_EXPOSE) domain_exposed_by_default = ( expose_by_default and state.domain in exposed_domains ) - # Expose an entity if the entity's domain is exposed by default and + # Expose an entity by default if the entity's domain is exposed by default + # and the entity is not a config or diagnostic entity + entity_exposed_by_default = domain_exposed_by_default and not auxiliary_entity + + # Expose an entity if the entity's is exposed by default and # the configuration doesn't explicitly exclude it from being # exposed, or if the entity is explicitly exposed - is_default_exposed = domain_exposed_by_default and explicit_expose is not False + is_default_exposed = entity_exposed_by_default and explicit_expose is not False return is_default_exposed or explicit_expose @@ -140,7 +156,7 @@ class GoogleConfig(AbstractConfig): ) _LOGGER.error("No configuration for request_sync available") - return HTTP_INTERNAL_SERVER_ERROR + return HTTPStatus.INTERNAL_SERVER_ERROR async def _async_update_token(self, force=False): if CONF_SERVICE_ACCOUNT not in self._config: @@ -181,7 +197,7 @@ class GoogleConfig(AbstractConfig): try: return await _call() except ClientResponseError as error: - if error.status == HTTP_UNAUTHORIZED: + if error.status == HTTPStatus.UNAUTHORIZED: _LOGGER.warning( "Request for %s unauthorized, renewing token and retrying", url ) @@ -193,7 +209,7 @@ class GoogleConfig(AbstractConfig): return error.status except (asyncio.TimeoutError, ClientError): _LOGGER.error("Could not contact %s", url) - return HTTP_INTERNAL_SERVER_ERROR + return HTTPStatus.INTERNAL_SERVER_ERROR async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index dc55509b534..c9f6c20c7af 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -45,9 +45,7 @@ async def _process(hass, data, message): "payload": {"errorCode": ERR_PROTOCOL_ERROR}, } - handler = HANDLERS.get(inputs[0].get("intent")) - - if handler is None: + if (handler := HANDLERS.get(inputs[0].get("intent"))) is None: return { "requestId": data.request_id, "payload": {"errorCode": ERR_PROTOCOL_ERROR}, @@ -131,9 +129,8 @@ async def async_devices_query(hass, data, payload): devices = {} for device in payload_devices: devid = device["id"] - state = hass.states.get(devid) - if not state: + if not (state := hass.states.get(devid)): # If we can't find a state, the device is offline devices[devid] = {"online": False} continue @@ -199,9 +196,7 @@ async def handle_devices_execute(hass, data, payload): executions[entity_id].append(execution) continue - state = hass.states.get(entity_id) - - if state is None: + if (state := hass.states.get(entity_id)) is None: results[entity_id] = { "ids": [entity_id], "status": "ERROR", diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index fea2ea4a310..9f79f0f7d9b 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -255,9 +255,7 @@ class BrightnessTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute a brightness command.""" - domain = self.state.domain - - if domain == light.DOMAIN: + if self.state.domain == light.DOMAIN: await self.hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_ON, @@ -348,9 +346,7 @@ class OnOffTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute an OnOff command.""" - domain = self.state.domain - - if domain == group.DOMAIN: + if (domain := self.state.domain) == group.DOMAIN: service_domain = HA_DOMAIN service = SERVICE_TURN_ON if params["on"] else SERVICE_TURN_OFF @@ -954,16 +950,14 @@ class TemperatureSettingTrait(_Trait): 1, ) else: - target_temp = attrs.get(ATTR_TEMPERATURE) - if target_temp is not None: + if (target_temp := attrs.get(ATTR_TEMPERATURE)) is not None: target_temp = round( temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 ) response["thermostatTemperatureSetpointHigh"] = target_temp response["thermostatTemperatureSetpointLow"] = target_temp else: - target_temp = attrs.get(ATTR_TEMPERATURE) - if target_temp is not None: + if (target_temp := attrs.get(ATTR_TEMPERATURE)) is not None: response["thermostatTemperatureSetpoint"] = round( temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 ) @@ -1158,9 +1152,7 @@ class HumiditySettingTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute a humidity command.""" - domain = self.state.domain - - if domain == sensor.DOMAIN: + if self.state.domain == sensor.DOMAIN: raise SmartHomeError( ERR_NOT_SUPPORTED, "Execute is not supported by sensor" ) @@ -1306,11 +1298,9 @@ class ArmDisArmTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute an ArmDisarm command.""" if params["arm"] and not params.get("cancel"): - arm_level = params.get("armLevel") - # If no arm level given, we can only arm it if there is # only one supported arm type. We never default to triggered. - if not arm_level: + if not (arm_level := params.get("armLevel")): states = self._supported_states() if STATE_ALARM_TRIGGERED in states: @@ -1453,8 +1443,7 @@ class FanSpeedTrait(_Trait): async def execute_reverse(self, data, params): """Execute a Reverse command.""" - domain = self.state.domain - if domain == fan.DOMAIN: + if self.state.domain == fan.DOMAIN: if self.state.attributes.get(fan.ATTR_DIRECTION) == fan.DIRECTION_FORWARD: direction = fan.DIRECTION_REVERSE else: @@ -1554,9 +1543,7 @@ class ModesTrait(_Trait): if self.state.domain != domain: continue - items = self.state.attributes.get(attr) - - if items is not None: + if (items := self.state.attributes.get(attr)) is not None: modes.append(self._generate(name, items)) # Shortcut since all domains are currently unique @@ -1668,19 +1655,19 @@ class ModesTrait(_Trait): ) 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, - ) + if self.state.domain == media_player.DOMAIN and ( + sound_mode := settings.get("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", @@ -2042,9 +2029,7 @@ def _verify_pin_challenge(data, state, challenge): if not challenge: raise ChallengeNeeded(CHALLENGE_PIN_NEEDED) - pin = challenge.get("pin") - - if pin != data.config.secure_devices_pin: + if challenge.get("pin") != data.config.secure_devices_pin: raise ChallengeNeeded(CHALLENGE_FAILED_PIN_NEEDED) @@ -2320,8 +2305,7 @@ class SensorStateTrait(_Trait): def sync_attributes(self): """Return attributes for a sync request.""" device_class = self.state.attributes.get(ATTR_DEVICE_CLASS) - data = self.sensor_types.get(device_class) - if data is not None: + if (data := self.sensor_types.get(device_class)) is not None: return { "sensorStatesSupported": { "name": data[0], @@ -2332,8 +2316,7 @@ class SensorStateTrait(_Trait): def query_attributes(self): """Return the attributes of this trait for this entity.""" device_class = self.state.attributes.get(ATTR_DEVICE_CLASS) - data = self.sensor_types.get(device_class) - if data is not None: + if (data := self.sensor_types.get(device_class)) is not None: return { "currentSensorStateData": [ {"name": data[0], "rawValue": self.state.state} diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index a1cbed2ee55..af4e6771795 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -150,8 +150,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_get_engine(hass, config, discovery_info=None): """Set up Google Cloud TTS component.""" - key_file = config.get(CONF_KEY_FILE) - if key_file: + if key_file := config.get(CONF_KEY_FILE): key_file = hass.config.path(key_file) if not os.path.isfile(key_file): _LOGGER.error("File %s doesn't exist", key_file) diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index 88fe587fbd0..7ac88b84727 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -25,6 +25,6 @@ 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.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py index cf5f6e8b0af..00d3119e868 100644 --- a/homeassistant/components/google_travel_time/helpers.py +++ b/homeassistant/components/google_travel_time/helpers.py @@ -30,9 +30,7 @@ def resolve_location(hass, logger, loc): def get_location_from_entity(hass, logger, entity_id): """Get the location from the entity state or attributes.""" - entity = hass.states.get(entity_id) - - if entity is None: + if (entity := hass.states.get(entity_id)) is None: logger.error("Unable to find entity %s", entity_id) return None diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index c8cb9d54510..1b999edc8b7 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util @@ -209,13 +210,13 @@ class GoogleTravelTimeSensor(SensorEntity): return None @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "name": DOMAIN, - "identifiers": {(DOMAIN, self._api_key)}, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self._api_key)}, + name=DOMAIN, + ) @property def unique_id(self) -> str: diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py index 0a26a514323..649e0283f5a 100644 --- a/homeassistant/components/gpmdp/media_player.py +++ b/homeassistant/components/gpmdp/media_player.py @@ -109,8 +109,7 @@ def request_configuration(hass, config, url, add_entities_callback): "the desktop player and try again" ) break - code = tmpmsg["payload"] - if code == "CODE_REQUIRED": + if (code := tmpmsg["payload"]) == "CODE_REQUIRED": continue setup_gpmdp(hass, config, code, add_entities_callback) save_json(hass.config.path(GPMDP_CONFIG_FILE), {"CODE": code}) diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 0c475872093..715119448b5 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -1,4 +1,6 @@ """Support for GPSLogger.""" +from http import HTTPStatus + from aiohttp import web import voluptuous as vol @@ -6,12 +8,7 @@ from homeassistant.components.device_tracker import ( ATTR_BATTERY, DOMAIN as DEVICE_TRACKER, ) -from homeassistant.const import ( - ATTR_LATITUDE, - ATTR_LONGITUDE, - CONF_WEBHOOK_ID, - HTTP_UNPROCESSABLE_ENTITY, -) +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -68,7 +65,9 @@ async def handle_webhook(hass, webhook_id, request): try: data = WEBHOOK_SCHEMA(dict(await request.post())) except vol.MultipleInvalid as error: - return web.Response(text=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY) + return web.Response( + text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY + ) attrs = { ATTR_SPEED: data.get(ATTR_SPEED), diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 2493054473a..8b0965cc434 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -11,6 +11,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.restore_state import RestoreEntity from . import DOMAIN as GPL_DOMAIN, TRACKER_UPDATE @@ -111,9 +112,9 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return {"name": self._name, "identifiers": {(GPL_DOMAIN, self._unique_id)}} + return DeviceInfo(identifiers={(GPL_DOMAIN, self._unique_id)}, name=self._name) @property def source_type(self): diff --git a/homeassistant/components/gpslogger/translations/bg.json b/homeassistant/components/gpslogger/translations/bg.json index 895cf27ee5b..8e1049d859e 100644 --- a/homeassistant/components/gpslogger/translations/bg.json +++ b/homeassistant/components/gpslogger/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 GPSLogger. \n\n \u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." }, diff --git a/homeassistant/components/gpslogger/translations/hu.json b/homeassistant/components/gpslogger/translations/hu.json index 45832cf493f..d458e959d0a 100644 --- a/homeassistant/components/gpslogger/translations/hu.json +++ b/homeassistant/components/gpslogger/translations/hu.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." + "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtani a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1lja: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." }, "step": { "user": { diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 9405b576b4d..b63e461e76a 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -149,8 +149,7 @@ class GraphiteFeeder(threading.Thread): def run(self): """Run the process to export the data.""" while True: - event = self._queue.get() - if event == self._quit_object: + if (event := self._queue.get()) == self._quit_object: _LOGGER.debug("Event processing thread stopped") self._queue.task_done() return diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index b91324ba4b3..761c8e0ab78 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -45,7 +45,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.""" if hass.data[DOMAIN].get(DISPATCHERS) is not None: for cleanup in hass.data[DOMAIN][DISPATCHERS]: diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 73ea66e5895..dbf8214e29a 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -50,6 +50,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -137,14 +138,14 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): return self._mac @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "name": self._name, - "identifiers": {(DOMAIN, self._mac)}, - "manufacturer": "Gree", - "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + identifiers={(DOMAIN, self._mac)}, + manufacturer="Gree", + name=self._name, + ) @property def temperature_unit(self) -> str: diff --git a/homeassistant/components/gree/entity.py b/homeassistant/components/gree/entity.py index 0753a780f4b..7407a90b4d0 100644 --- a/homeassistant/components/gree/entity.py +++ b/homeassistant/components/gree/entity.py @@ -1,5 +1,6 @@ """Entity object for shared properties of Gree entities.""" from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .bridge import DeviceDataUpdateCoordinator @@ -27,11 +28,11 @@ class GreeEntity(CoordinatorEntity): return f"{self._mac}_{self._desc}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return info about the device.""" - return { - "identifiers": {(DOMAIN, self._mac)}, - "name": self._name, - "manufacturer": "Gree", - "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + identifiers={(DOMAIN, self._mac)}, + manufacturer="Gree", + name=self._name, + ) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index e62bf402523..62d5bec6bb8 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.8"], + "requirements": ["greeclimate==0.12.3"], "codeowners": ["@cmroche"], "iot_class": "local_polling" } diff --git a/homeassistant/components/gree/translations/bg.json b/homeassistant/components/gree/translations/bg.json new file mode 100644 index 00000000000..e7ed81d36f5 --- /dev/null +++ b/homeassistant/components/gree/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index 51471739e98..cc7b8955756 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -157,8 +157,7 @@ async def async_setup(hass, config): } ) - sensor_configs = monitor_config[CONF_TEMPERATURE_SENSORS] - if sensor_configs: + if sensor_configs := monitor_config[CONF_TEMPERATURE_SENSORS]: temperature_unit = { CONF_TEMPERATURE_UNIT: sensor_configs[CONF_TEMPERATURE_UNIT] } diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index dad8f943328..523b45a94f7 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -56,7 +56,7 @@ ATTR_ALL = "all" SERVICE_SET = "set" SERVICE_REMOVE = "remove" -PLATFORMS = ["light", "cover", "notify", "binary_sensor"] +PLATFORMS = ["light", "cover", "notify", "fan", "binary_sensor"] REG_KEY = f"{DOMAIN}_registry" @@ -121,9 +121,7 @@ def is_on(hass, entity_id): # Integration not setup yet, it cannot be on return False - state = hass.states.get(entity_id) - - if state is not None: + if (state := hass.states.get(entity_id)) is not None: return state.state in hass.data[REG_KEY].on_off_mapping return False @@ -213,9 +211,7 @@ def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: async def async_setup(hass, config): """Set up all groups found defined in the configuration.""" - component = hass.data.get(DOMAIN) - - if component is None: + if (component := hass.data.get(DOMAIN)) is None: component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) hass.data[REG_KEY] = GroupIntegrationRegistry() @@ -507,9 +503,7 @@ class Group(Entity): ) # If called before the platform async_setup is called (test cases) - component = hass.data.get(DOMAIN) - - if component is None: + if (component := hass.data.get(DOMAIN)) is None: component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) await component.async_add_entities([group]) @@ -661,9 +655,8 @@ class Group(Entity): return self.async_set_context(event.context) - new_state = event.data.get("new_state") - if new_state is None: + if (new_state := event.data.get("new_state")) is None: # The state was removed from the state machine self._reset_tracked_state() @@ -677,9 +670,7 @@ class Group(Entity): self._on_states = set() for entity_id in self.trackable: - state = self.hass.states.get(entity_id) - - if state is not None: + if (state := self.hass.states.get(entity_id)) is not None: self._see_state(state) def _see_state(self, new_state): diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 24d6cb86aa1..613b21571de 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -1,7 +1,6 @@ """This platform allows several binary sensor to be grouped into one binary sensor.""" from __future__ import annotations -import logging from typing import Any import voluptuous as vol @@ -34,8 +33,6 @@ DEFAULT_NAME = "Binary Sensor Group" CONF_ALL = "all" REG_KEY = f"{BINARY_SENSOR_DOMAIN}_registry" -_LOGGER = logging.getLogger(__name__) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(BINARY_SENSOR_DOMAIN), diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 68ad61c33fc..8c4e260b8c1 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -112,8 +112,7 @@ class CoverGroup(GroupEntity, CoverEntity): async def _update_supported_features_event(self, event: Event) -> None: self.async_set_context(event.context) - entity = event.data.get("entity_id") - if entity is not None: + if (entity := event.data.get("entity_id")) is not None: await self.async_update_supported_features( entity, event.data.get("new_state") ) @@ -168,8 +167,7 @@ class CoverGroup(GroupEntity, CoverEntity): async def async_added_to_hass(self) -> None: """Register listeners.""" for entity_id in self._entities: - new_state = self.hass.states.get(entity_id) - if new_state is None: + if (new_state := self.hass.states.get(entity_id)) is None: continue await self.async_update_supported_features( entity_id, new_state, update_state=False @@ -264,8 +262,7 @@ class CoverGroup(GroupEntity, CoverEntity): self._attr_is_opening = False has_valid_state = False for entity_id in self._entities: - state = self.hass.states.get(entity_id) - if not state: + if not (state := self.hass.states.get(entity_id)): continue if state.state == STATE_OPEN: self._attr_is_closed = False @@ -322,8 +319,7 @@ class CoverGroup(GroupEntity, CoverEntity): if not self._attr_assumed_state: for entity_id in self._entities: - state = self.hass.states.get(entity_id) - if state is None: + if (state := self.hass.states.get(entity_id)) is None: continue if state and state.attributes.get(ATTR_ASSUMED_STATE): self._attr_assumed_state = True diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py new file mode 100644 index 00000000000..d36fcc39f43 --- /dev/null +++ b/homeassistant/components/group/fan.py @@ -0,0 +1,284 @@ +"""This platform allows several fans to be grouped into one fan.""" +from __future__ import annotations + +from functools import reduce +import logging +from operator import ior +from typing import Any + +import voluptuous as vol + +from homeassistant.components.fan import ( + ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_PERCENTAGE, + ATTR_PERCENTAGE_STEP, + DOMAIN, + PLATFORM_SCHEMA, + SERVICE_OSCILLATE, + SERVICE_SET_DIRECTION, + SERVICE_SET_PERCENTAGE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SUPPORT_DIRECTION, + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, + FanEntity, +) +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, + CONF_NAME, + CONF_UNIQUE_ID, + STATE_ON, +) +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 +from .util import ( + attribute_equal, + most_frequent_attribute, + reduce_attribute, + states_equal, +) + +SUPPORTED_FLAGS = {SUPPORT_SET_SPEED, SUPPORT_DIRECTION, SUPPORT_OSCILLATE} + +DEFAULT_NAME = "Fan Group" + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: dict[str, Any] | None = None, +) -> None: + """Set up the Group Cover platform.""" + async_add_entities( + [FanGroup(config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES])] + ) + + +class FanGroup(GroupEntity, FanEntity): + """Representation of a FanGroup.""" + + _attr_assumed_state: bool = True + + def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: + """Initialize a FanGroup entity.""" + self._entities = entities + self._fans: dict[int, set[str]] = {flag: set() for flag in SUPPORTED_FLAGS} + self._percentage = None + self._oscillating = None + self._direction = None + self._supported_features = 0 + self._speed_count = 100 + self._is_on = False + self._attr_name = name + self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities} + self._attr_unique_id = unique_id + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return self._speed_count + + @property + def is_on(self) -> bool: + """Return true if the entity is on.""" + return self._is_on + + @property + def percentage(self) -> int | None: + """Return the current speed as a percentage.""" + return self._percentage + + @property + def current_direction(self) -> str | None: + """Return the current direction of the fan.""" + return self._direction + + @property + def oscillating(self) -> bool | None: + """Return whether or not the fan is currently oscillating.""" + return self._oscillating + + async def _update_supported_features_event(self, event: Event) -> None: + self.async_set_context(event.context) + if (entity := event.data.get("entity_id")) is not None: + await self.async_update_supported_features( + entity, event.data.get("new_state") + ) + + async def async_update_supported_features( + self, + entity_id: str, + new_state: State | None, + update_state: bool = True, + ) -> None: + """Update dictionaries with supported features.""" + if not new_state: + for values in self._fans.values(): + values.discard(entity_id) + else: + features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + for feature in SUPPORTED_FLAGS: + if features & feature: + self._fans[feature].add(entity_id) + else: + self._fans[feature].discard(entity_id) + + if update_state: + await self.async_defer_or_update_ha_state() + + async def async_added_to_hass(self) -> None: + """Register listeners.""" + for entity_id in self._entities: + if (new_state := self.hass.states.get(entity_id)) is None: + continue + await self.async_update_supported_features( + entity_id, new_state, update_state=False + ) + self.async_on_remove( + async_track_state_change_event( + self.hass, self._entities, self._update_supported_features_event + ) + ) + + if self.hass.state == CoreState.running: + await self.async_update() + return + await super().async_added_to_hass() + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed of the fan, as a percentage.""" + if percentage == 0: + await self.async_turn_off() + await self._async_call_supported_entities( + SERVICE_SET_PERCENTAGE, SUPPORT_SET_SPEED, {ATTR_PERCENTAGE: percentage} + ) + + async def async_oscillate(self, oscillating: bool) -> None: + """Oscillate the fan.""" + await self._async_call_supported_entities( + SERVICE_OSCILLATE, SUPPORT_OSCILLATE, {ATTR_OSCILLATING: oscillating} + ) + + async def async_set_direction(self, direction: str) -> None: + """Set the direction of the fan.""" + await self._async_call_supported_entities( + SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, {ATTR_DIRECTION: direction} + ) + + async def async_turn_on( + self, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the fan.""" + if percentage is not None: + await self.async_set_percentage(percentage) + return + await self._async_call_all_entities(SERVICE_TURN_ON) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the fans off.""" + await self._async_call_all_entities(SERVICE_TURN_OFF) + + async def _async_call_supported_entities( + self, service: str, support_flag: int, data: dict[str, Any] + ) -> None: + """Call a service with all entities.""" + await self.hass.services.async_call( + DOMAIN, + service, + {**data, ATTR_ENTITY_ID: self._fans[support_flag]}, + blocking=True, + context=self._context, + ) + + async def _async_call_all_entities(self, service: str) -> None: + """Call a service with all entities.""" + await self.hass.services.async_call( + DOMAIN, + service, + {ATTR_ENTITY_ID: self._entities}, + blocking=True, + context=self._context, + ) + + def _async_states_by_support_flag(self, flag: int) -> list[State]: + """Return all the entity states for a supported flag.""" + states: list[State] = list( + filter(None, [self.hass.states.get(x) for x in self._fans[flag]]) + ) + return states + + def _set_attr_most_frequent(self, attr: str, flag: int, entity_attr: str) -> None: + """Set an attribute based on most frequent supported entities attributes.""" + states = self._async_states_by_support_flag(flag) + setattr(self, attr, most_frequent_attribute(states, entity_attr)) + self._attr_assumed_state |= not attribute_equal(states, entity_attr) + + async def async_update(self) -> None: + """Update state and attributes.""" + self._attr_assumed_state = False + + on_states: list[State] = list( + filter(None, [self.hass.states.get(x) for x in self._entities]) + ) + self._is_on = any(state.state == STATE_ON for state in on_states) + self._attr_assumed_state |= not states_equal(on_states) + + percentage_states = self._async_states_by_support_flag(SUPPORT_SET_SPEED) + self._percentage = reduce_attribute(percentage_states, ATTR_PERCENTAGE) + self._attr_assumed_state |= not attribute_equal( + percentage_states, ATTR_PERCENTAGE + ) + if ( + percentage_states + and percentage_states[0].attributes.get(ATTR_PERCENTAGE_STEP) + and attribute_equal(percentage_states, ATTR_PERCENTAGE_STEP) + ): + self._speed_count = ( + round(100 / percentage_states[0].attributes[ATTR_PERCENTAGE_STEP]) + or 100 + ) + else: + self._speed_count = 100 + + self._set_attr_most_frequent( + "_oscillating", SUPPORT_OSCILLATE, ATTR_OSCILLATING + ) + self._set_attr_most_frequent("_direction", SUPPORT_DIRECTION, ATTR_DIRECTION) + + self._supported_features = reduce( + ior, [feature for feature in SUPPORTED_FLAGS if self._fans[feature]], 0 + ) + self._attr_assumed_state |= any( + state.attributes.get(ATTR_ASSUMED_STATE) for state in on_states + ) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index a3a02ee6b9c..4a14bc5dcf3 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import Counter import itertools +import logging from typing import Any, Set, cast import voluptuous as vol @@ -66,6 +67,8 @@ SUPPORT_GROUP_LIGHT = ( SUPPORT_EFFECT | SUPPORT_FLASH | SUPPORT_TRANSITION | SUPPORT_WHITE_VALUE ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform( hass: HomeAssistant, @@ -152,6 +155,8 @@ class LightGroup(GroupEntity, light.LightEntity): } data[ATTR_ENTITY_ID] = self._entity_ids + _LOGGER.debug("Forwarded turn_on command: %s", data) + await self.hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_ON, diff --git a/homeassistant/components/group/translations/ca.json b/homeassistant/components/group/translations/ca.json index 552a2c9677e..5cb406727e8 100644 --- a/homeassistant/components/group/translations/ca.json +++ b/homeassistant/components/group/translations/ca.json @@ -5,9 +5,9 @@ "home": "A casa", "locked": "Bloquejat", "not_home": "Fora", - "off": "OFF", + "off": "off", "ok": "OK", - "on": "ON", + "on": "on", "open": "Obert/a", "problem": "Problema", "unlocked": "Desbloquejat" diff --git a/homeassistant/components/group/translations/hr.json b/homeassistant/components/group/translations/hr.json index 85abe33638b..fbf123b0e88 100644 --- a/homeassistant/components/group/translations/hr.json +++ b/homeassistant/components/group/translations/hr.json @@ -5,7 +5,7 @@ "home": "Doma", "locked": "Zaklju\u010dano", "not_home": "Odsutan", - "off": "Uklju\u010deno", + "off": "Isklju\u010deno", "ok": "U redu", "on": "Uklju\u010deno", "open": "Otvoreno", diff --git a/homeassistant/components/group/util.py b/homeassistant/components/group/util.py index 0944ceb6745..da67e071f27 100644 --- a/homeassistant/components/group/util.py +++ b/homeassistant/components/group/util.py @@ -11,11 +11,16 @@ from homeassistant.core import State def find_state_attributes(states: list[State], key: str) -> Iterator[Any]: """Find attributes with matching key from states.""" for state in states: - value = state.attributes.get(key) - if value is not None: + if (value := state.attributes.get(key)) is not None: yield value +def find_state(states: list[State]) -> Iterator[Any]: + """Find state from states.""" + for state in states: + yield state.state + + def mean_int(*args: Any) -> int: """Return the mean of the supplied values.""" return int(sum(args) / len(args)) @@ -31,8 +36,30 @@ def attribute_equal(states: list[State], key: str) -> bool: Note: Returns True if no matching attribute is found. """ - attrs = find_state_attributes(states, key) - grp = groupby(attrs) + return _values_equal(find_state_attributes(states, key)) + + +def most_frequent_attribute(states: list[State], key: str) -> Any | None: + """Find attributes with matching key from states.""" + if attrs := list(find_state_attributes(states, key)): + return max(set(attrs), key=attrs.count) + return None + + +def states_equal(states: list[State]) -> bool: + """Return True if all states are equal. + + Note: Returns True if no matching attribute is found. + """ + return _values_equal(find_state(states)) + + +def _values_equal(values: Iterator[Any]) -> bool: + """Return True if all values are equal. + + Note: Returns True if no matching attribute is found. + """ + grp = groupby(values) return bool(next(grp, True) and not next(grp, False)) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 804d4157543..3ad0044f93e 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -1,875 +1,36 @@ """Read status of growatt inverters.""" from __future__ import annotations -from dataclasses import dataclass import datetime import json import logging import growattServer -from homeassistant.components.sensor import ( - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import ( - CONF_NAME, - CONF_PASSWORD, - CONF_URL, - CONF_USERNAME, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, - DEVICE_CLASS_VOLTAGE, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - FREQUENCY_HERTZ, - PERCENTAGE, - POWER_KILO_WATT, - POWER_WATT, - TEMP_CELSIUS, -) +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import Throttle, dt -from .const import CONF_PLANT_ID, DEFAULT_PLANT_ID, DEFAULT_URL, LOGIN_INVALID_AUTH_CODE +from .const import ( + CONF_PLANT_ID, + DEFAULT_PLANT_ID, + DEFAULT_URL, + DOMAIN, + LOGIN_INVALID_AUTH_CODE, +) +from .sensor_types.inverter import INVERTER_SENSOR_TYPES +from .sensor_types.mix import MIX_SENSOR_TYPES +from .sensor_types.sensor_entity_description import GrowattSensorEntityDescription +from .sensor_types.storage import STORAGE_SENSOR_TYPES +from .sensor_types.tlx import TLX_SENSOR_TYPES +from .sensor_types.total import TOTAL_SENSOR_TYPES _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(minutes=1) -@dataclass -class GrowattRequiredKeysMixin: - """Mixin for required keys.""" - - api_key: str - - -@dataclass -class GrowattSensorEntityDescription(SensorEntityDescription, GrowattRequiredKeysMixin): - """Describes Growatt sensor entity.""" - - precision: int | None = None - currency: bool = False - - -TOTAL_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( - GrowattSensorEntityDescription( - key="total_money_today", - name="Total money today", - api_key="plantMoneyText", - currency=True, - ), - GrowattSensorEntityDescription( - key="total_money_total", - name="Money lifetime", - api_key="totalMoneyText", - currency=True, - ), - GrowattSensorEntityDescription( - key="total_energy_today", - name="Energy Today", - api_key="todayEnergy", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="total_output_power", - name="Output Power", - api_key="invTodayPpv", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="total_energy_output", - name="Lifetime energy output", - api_key="totalEnergy", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - ), - GrowattSensorEntityDescription( - key="total_maximum_output", - name="Maximum power", - api_key="nominalPower", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - ), -) - -INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( - GrowattSensorEntityDescription( - key="inverter_energy_today", - name="Energy today", - api_key="powerToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_energy_total", - name="Lifetime energy output", - api_key="powerTotal", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - precision=1, - state_class=STATE_CLASS_TOTAL, - ), - GrowattSensorEntityDescription( - key="inverter_voltage_input_1", - name="Input 1 voltage", - api_key="vpv1", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - precision=2, - ), - GrowattSensorEntityDescription( - key="inverter_amperage_input_1", - name="Input 1 Amperage", - api_key="ipv1", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_wattage_input_1", - name="Input 1 Wattage", - api_key="ppv1", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_voltage_input_2", - name="Input 2 voltage", - api_key="vpv2", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_amperage_input_2", - name="Input 2 Amperage", - api_key="ipv2", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_wattage_input_2", - name="Input 2 Wattage", - api_key="ppv2", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_voltage_input_3", - name="Input 3 voltage", - api_key="vpv3", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_amperage_input_3", - name="Input 3 Amperage", - api_key="ipv3", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_wattage_input_3", - name="Input 3 Wattage", - api_key="ppv3", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_internal_wattage", - name="Internal wattage", - api_key="ppv", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_reactive_voltage", - name="Reactive voltage", - api_key="vacr", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_inverter_reactive_amperage", - name="Reactive amperage", - api_key="iacr", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_frequency", - name="AC frequency", - api_key="fac", - native_unit_of_measurement=FREQUENCY_HERTZ, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_current_wattage", - name="Output power", - api_key="pac", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_current_reactive_wattage", - name="Reactive wattage", - api_key="pacr", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_ipm_temperature", - name="Intelligent Power Management temperature", - api_key="ipmTemperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - precision=1, - ), - GrowattSensorEntityDescription( - key="inverter_temperature", - name="Temperature", - api_key="temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - precision=1, - ), -) - -TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( - GrowattSensorEntityDescription( - key="tlx_energy_today", - name="Energy today", - api_key="eacToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_energy_total", - name="Lifetime energy output", - api_key="eacTotal", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_energy_total_input_1", - name="Lifetime total energy input 1", - api_key="epv1Total", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_energy_today_input_1", - name="Energy Today Input 1", - api_key="epv1Today", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_voltage_input_1", - name="Input 1 voltage", - api_key="vpv1", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_amperage_input_1", - name="Input 1 Amperage", - api_key="ipv1", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_wattage_input_1", - name="Input 1 Wattage", - api_key="ppv1", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_energy_total_input_2", - name="Lifetime total energy input 2", - api_key="epv2Total", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_energy_today_input_2", - name="Energy Today Input 2", - api_key="epv2Today", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_voltage_input_2", - name="Input 2 voltage", - api_key="vpv2", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_amperage_input_2", - name="Input 2 Amperage", - api_key="ipv2", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_wattage_input_2", - name="Input 2 Wattage", - api_key="ppv2", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_internal_wattage", - name="Internal wattage", - api_key="ppv", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_reactive_voltage", - name="Reactive voltage", - api_key="vacrs", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_frequency", - name="AC frequency", - api_key="fac", - native_unit_of_measurement=FREQUENCY_HERTZ, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_current_wattage", - name="Output power", - api_key="pac", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_temperature_1", - name="Temperature 1", - api_key="temp1", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_temperature_2", - name="Temperature 2", - api_key="temp2", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_temperature_3", - name="Temperature 3", - api_key="temp3", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_temperature_4", - name="Temperature 4", - api_key="temp4", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - precision=1, - ), - GrowattSensorEntityDescription( - key="tlx_temperature_5", - name="Temperature 5", - api_key="temp5", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - precision=1, - ), -) - -STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( - GrowattSensorEntityDescription( - key="storage_storage_production_today", - name="Storage production today", - api_key="eBatDisChargeToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="storage_storage_production_lifetime", - name="Lifetime Storage production", - api_key="eBatDisChargeTotal", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - ), - GrowattSensorEntityDescription( - key="storage_grid_discharge_today", - name="Grid discharged today", - api_key="eacDisChargeToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="storage_load_consumption_today", - name="Load consumption today", - api_key="eopDischrToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="storage_load_consumption_lifetime", - name="Lifetime load consumption", - api_key="eopDischrTotal", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - ), - GrowattSensorEntityDescription( - key="storage_grid_charged_today", - name="Grid charged today", - api_key="eacChargeToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="storage_charge_storage_lifetime", - name="Lifetime storaged charged", - api_key="eChargeTotal", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - ), - GrowattSensorEntityDescription( - key="storage_solar_production", - name="Solar power production", - api_key="ppv", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="storage_battery_percentage", - name="Battery percentage", - api_key="capacity", - native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - ), - GrowattSensorEntityDescription( - key="storage_power_flow", - name="Storage charging/ discharging(-ve)", - api_key="pCharge", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="storage_load_consumption_solar_storage", - name="Load consumption(Solar + Storage)", - api_key="rateVA", - native_unit_of_measurement="VA", - ), - GrowattSensorEntityDescription( - key="storage_charge_today", - name="Charge today", - api_key="eChargeToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="storage_import_from_grid", - name="Import from grid", - api_key="pAcInPut", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="storage_import_from_grid_today", - name="Import from grid today", - api_key="eToUserToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="storage_import_from_grid_total", - name="Import from grid total", - api_key="eToUserTotal", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - ), - GrowattSensorEntityDescription( - key="storage_load_consumption", - name="Load consumption", - api_key="outPutPower", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="storage_grid_voltage", - name="AC input voltage", - api_key="vGrid", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - precision=2, - ), - GrowattSensorEntityDescription( - key="storage_pv_charging_voltage", - name="PV charging voltage", - api_key="vpv", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - precision=2, - ), - GrowattSensorEntityDescription( - key="storage_ac_input_frequency_out", - name="AC input frequency", - api_key="freqOutPut", - native_unit_of_measurement=FREQUENCY_HERTZ, - precision=2, - ), - GrowattSensorEntityDescription( - key="storage_output_voltage", - name="Output voltage", - api_key="outPutVolt", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - precision=2, - ), - GrowattSensorEntityDescription( - key="storage_ac_output_frequency", - name="Ac output frequency", - api_key="freqGrid", - native_unit_of_measurement=FREQUENCY_HERTZ, - precision=2, - ), - GrowattSensorEntityDescription( - key="storage_current_PV", - name="Solar charge current", - api_key="iAcCharge", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - precision=2, - ), - GrowattSensorEntityDescription( - key="storage_current_1", - name="Solar current to storage", - api_key="iChargePV1", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - precision=2, - ), - GrowattSensorEntityDescription( - key="storage_grid_amperage_input", - name="Grid charge current", - api_key="chgCurr", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - precision=2, - ), - GrowattSensorEntityDescription( - key="storage_grid_out_current", - name="Grid out current", - api_key="outPutCurrent", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - precision=2, - ), - GrowattSensorEntityDescription( - key="storage_battery_voltage", - name="Battery voltage", - api_key="vBat", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - precision=2, - ), - GrowattSensorEntityDescription( - key="storage_load_percentage", - name="Load percentage", - api_key="loadPercent", - native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - precision=2, - ), -) - -MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( - # Values from 'mix_info' API call - GrowattSensorEntityDescription( - key="mix_statement_of_charge", - name="Statement of charge", - api_key="capacity", - native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - ), - GrowattSensorEntityDescription( - key="mix_battery_charge_today", - name="Battery charged today", - api_key="eBatChargeToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="mix_battery_charge_lifetime", - name="Lifetime battery charged", - api_key="eBatChargeTotal", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - ), - GrowattSensorEntityDescription( - key="mix_battery_discharge_today", - name="Battery discharged today", - api_key="eBatDisChargeToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="mix_battery_discharge_lifetime", - name="Lifetime battery discharged", - api_key="eBatDisChargeTotal", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - ), - GrowattSensorEntityDescription( - key="mix_solar_generation_today", - name="Solar energy today", - api_key="epvToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="mix_solar_generation_lifetime", - name="Lifetime solar energy", - api_key="epvTotal", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - ), - GrowattSensorEntityDescription( - key="mix_battery_discharge_w", - name="Battery discharging W", - api_key="pDischarge1", - native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="mix_battery_voltage", - name="Battery voltage", - api_key="vbat", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - ), - GrowattSensorEntityDescription( - key="mix_pv1_voltage", - name="PV1 voltage", - api_key="vpv1", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - ), - GrowattSensorEntityDescription( - key="mix_pv2_voltage", - name="PV2 voltage", - api_key="vpv2", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - ), - # Values from 'mix_totals' API call - GrowattSensorEntityDescription( - key="mix_load_consumption_today", - name="Load consumption today", - api_key="elocalLoadToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="mix_load_consumption_lifetime", - name="Lifetime load consumption", - api_key="elocalLoadTotal", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - ), - GrowattSensorEntityDescription( - key="mix_export_to_grid_today", - name="Export to grid today", - api_key="etoGridToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="mix_export_to_grid_lifetime", - name="Lifetime export to grid", - api_key="etogridTotal", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, - ), - # Values from 'mix_system_status' API call - GrowattSensorEntityDescription( - key="mix_battery_charge", - name="Battery charging", - api_key="chargePower", - native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="mix_load_consumption", - name="Load consumption", - api_key="pLocalLoad", - native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="mix_wattage_pv_1", - name="PV1 Wattage", - api_key="pPv1", - native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="mix_wattage_pv_2", - name="PV2 Wattage", - api_key="pPv2", - native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="mix_wattage_pv_all", - name="All PV Wattage", - api_key="ppv", - native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="mix_export_to_grid", - name="Export to grid", - api_key="pactogrid", - native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="mix_import_from_grid", - name="Import from grid", - api_key="pactouser", - native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="mix_battery_discharge_kw", - name="Battery discharging kW", - api_key="pdisCharge1", - native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, - ), - GrowattSensorEntityDescription( - key="mix_grid_voltage", - name="Grid voltage", - api_key="vAc1", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - ), - # Values from 'mix_detail' API call - GrowattSensorEntityDescription( - key="mix_system_production_today", - name="System production today (self-consumption + export)", - api_key="eCharge", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="mix_load_consumption_solar_today", - name="Load consumption today (solar)", - api_key="eChargeToday", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="mix_self_consumption_today", - name="Self consumption today (solar + battery)", - api_key="eChargeToday1", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="mix_load_consumption_battery_today", - name="Load consumption today (battery)", - api_key="echarge1", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - GrowattSensorEntityDescription( - key="mix_import_from_grid_today", - name="Import from grid today (load)", - api_key="etouser", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - # This sensor is manually created using the most recent X-Axis value from the chartData - GrowattSensorEntityDescription( - key="mix_last_update", - name="Last Data Update", - api_key="lastdataupdate", - native_unit_of_measurement=None, - device_class=DEVICE_CLASS_TIMESTAMP, - ), - # Values from 'dashboard_data' API call - GrowattSensorEntityDescription( - key="mix_import_from_grid_today_combined", - name="Import from grid today (load + charging)", - api_key="etouser_combined", # This id is not present in the raw API data, it is added by the sensor - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), -) - - def get_device_list(api, config): """Retrieve the device list for the selected plant.""" plant_id = config[CONF_PLANT_ID] @@ -970,6 +131,12 @@ class GrowattInverter(SensorEntity): self._attr_unique_id = unique_id self._attr_icon = "mdi:solar-power" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, probe.device_id)}, + manufacturer="Growatt", + name=name, + ) + @property def native_value(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/growatt_server/sensor_types/inverter.py b/homeassistant/components/growatt_server/sensor_types/inverter.py new file mode 100644 index 00000000000..709ea81b3c5 --- /dev/null +++ b/homeassistant/components/growatt_server/sensor_types/inverter.py @@ -0,0 +1,174 @@ +"""Growatt Sensor definitions for the Inverter type.""" +from __future__ import annotations + +from homeassistant.components.sensor import STATE_CLASS_TOTAL +from homeassistant.const import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + FREQUENCY_HERTZ, + POWER_WATT, + TEMP_CELSIUS, +) + +from .sensor_entity_description import GrowattSensorEntityDescription + +INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( + GrowattSensorEntityDescription( + key="inverter_energy_today", + name="Energy today", + api_key="powerToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_energy_total", + name="Lifetime energy output", + api_key="powerTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + precision=1, + state_class=STATE_CLASS_TOTAL, + ), + GrowattSensorEntityDescription( + key="inverter_voltage_input_1", + name="Input 1 voltage", + api_key="vpv1", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="inverter_amperage_input_1", + name="Input 1 Amperage", + api_key="ipv1", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_wattage_input_1", + name="Input 1 Wattage", + api_key="ppv1", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_voltage_input_2", + name="Input 2 voltage", + api_key="vpv2", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_amperage_input_2", + name="Input 2 Amperage", + api_key="ipv2", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_wattage_input_2", + name="Input 2 Wattage", + api_key="ppv2", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_voltage_input_3", + name="Input 3 voltage", + api_key="vpv3", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_amperage_input_3", + name="Input 3 Amperage", + api_key="ipv3", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_wattage_input_3", + name="Input 3 Wattage", + api_key="ppv3", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_internal_wattage", + name="Internal wattage", + api_key="ppv", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_reactive_voltage", + name="Reactive voltage", + api_key="vacr", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_inverter_reactive_amperage", + name="Reactive amperage", + api_key="iacr", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_frequency", + name="AC frequency", + api_key="fac", + native_unit_of_measurement=FREQUENCY_HERTZ, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_current_wattage", + name="Output power", + api_key="pac", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_current_reactive_wattage", + name="Reactive wattage", + api_key="pacr", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_ipm_temperature", + name="Intelligent Power Management temperature", + api_key="ipmTemperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_temperature", + name="Temperature", + api_key="temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + precision=1, + ), +) diff --git a/homeassistant/components/growatt_server/sensor_types/mix.py b/homeassistant/components/growatt_server/sensor_types/mix.py new file mode 100644 index 00000000000..939da82902a --- /dev/null +++ b/homeassistant/components/growatt_server/sensor_types/mix.py @@ -0,0 +1,253 @@ +"""Growatt Sensor definitions for the Mix type.""" +from __future__ import annotations + +from homeassistant.components.sensor import ( + STATE_CLASS_TOTAL, + STATE_CLASS_TOTAL_INCREASING, +) +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + PERCENTAGE, + POWER_KILO_WATT, + POWER_WATT, +) + +from .sensor_entity_description import GrowattSensorEntityDescription + +MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( + # Values from 'mix_info' API call + GrowattSensorEntityDescription( + key="mix_statement_of_charge", + name="Statement of charge", + api_key="capacity", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + ), + GrowattSensorEntityDescription( + key="mix_battery_charge_today", + name="Battery charged today", + api_key="eBatChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_battery_charge_lifetime", + name="Lifetime battery charged", + api_key="eBatChargeTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + ), + GrowattSensorEntityDescription( + key="mix_battery_discharge_today", + name="Battery discharged today", + api_key="eBatDisChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_battery_discharge_lifetime", + name="Lifetime battery discharged", + api_key="eBatDisChargeTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + ), + GrowattSensorEntityDescription( + key="mix_solar_generation_today", + name="Solar energy today", + api_key="epvToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_solar_generation_lifetime", + name="Lifetime solar energy", + api_key="epvTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + ), + GrowattSensorEntityDescription( + key="mix_battery_discharge_w", + name="Battery discharging W", + api_key="pDischarge1", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_battery_voltage", + name="Battery voltage", + api_key="vbat", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + GrowattSensorEntityDescription( + key="mix_pv1_voltage", + name="PV1 voltage", + api_key="vpv1", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + GrowattSensorEntityDescription( + key="mix_pv2_voltage", + name="PV2 voltage", + api_key="vpv2", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + # Values from 'mix_totals' API call + GrowattSensorEntityDescription( + key="mix_load_consumption_today", + name="Load consumption today", + api_key="elocalLoadToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_load_consumption_lifetime", + name="Lifetime load consumption", + api_key="elocalLoadTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + ), + GrowattSensorEntityDescription( + key="mix_export_to_grid_today", + name="Export to grid today", + api_key="etoGridToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_export_to_grid_lifetime", + name="Lifetime export to grid", + api_key="etogridTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + ), + # Values from 'mix_system_status' API call + GrowattSensorEntityDescription( + key="mix_battery_charge", + name="Battery charging", + api_key="chargePower", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_load_consumption", + name="Load consumption", + api_key="pLocalLoad", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_wattage_pv_1", + name="PV1 Wattage", + api_key="pPv1", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_wattage_pv_2", + name="PV2 Wattage", + api_key="pPv2", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_wattage_pv_all", + name="All PV Wattage", + api_key="ppv", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_export_to_grid", + name="Export to grid", + api_key="pactogrid", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_import_from_grid", + name="Import from grid", + api_key="pactouser", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_battery_discharge_kw", + name="Battery discharging kW", + api_key="pdisCharge1", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_grid_voltage", + name="Grid voltage", + api_key="vAc1", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + # Values from 'mix_detail' API call + GrowattSensorEntityDescription( + key="mix_system_production_today", + name="System production today (self-consumption + export)", + api_key="eCharge", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_load_consumption_solar_today", + name="Load consumption today (solar)", + api_key="eChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_self_consumption_today", + name="Self consumption today (solar + battery)", + api_key="eChargeToday1", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_load_consumption_battery_today", + name="Load consumption today (battery)", + api_key="echarge1", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_import_from_grid_today", + name="Import from grid today (load)", + api_key="etouser", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + # This sensor is manually created using the most recent X-Axis value from the chartData + GrowattSensorEntityDescription( + key="mix_last_update", + name="Last Data Update", + api_key="lastdataupdate", + native_unit_of_measurement=None, + device_class=DEVICE_CLASS_TIMESTAMP, + ), + # Values from 'dashboard_data' API call + GrowattSensorEntityDescription( + key="mix_import_from_grid_today_combined", + name="Import from grid today (load + charging)", + api_key="etouser_combined", # This id is not present in the raw API data, it is added by the sensor + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), +) diff --git a/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py b/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py new file mode 100644 index 00000000000..04822fca35b --- /dev/null +++ b/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py @@ -0,0 +1,21 @@ +"""Sensor Entity Description for the Growatt integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.sensor import SensorEntityDescription + + +@dataclass +class GrowattRequiredKeysMixin: + """Mixin for required keys.""" + + api_key: str + + +@dataclass +class GrowattSensorEntityDescription(SensorEntityDescription, GrowattRequiredKeysMixin): + """Describes Growatt sensor entity.""" + + precision: int | None = None + currency: bool = False diff --git a/homeassistant/components/growatt_server/sensor_types/storage.py b/homeassistant/components/growatt_server/sensor_types/storage.py new file mode 100644 index 00000000000..77d1b4b2c00 --- /dev/null +++ b/homeassistant/components/growatt_server/sensor_types/storage.py @@ -0,0 +1,223 @@ +"""Growatt Sensor definitions for the Storage type.""" +from __future__ import annotations + +from homeassistant.components.sensor import STATE_CLASS_TOTAL +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + FREQUENCY_HERTZ, + PERCENTAGE, + POWER_WATT, +) + +from .sensor_entity_description import GrowattSensorEntityDescription + +STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( + GrowattSensorEntityDescription( + key="storage_storage_production_today", + name="Storage production today", + api_key="eBatDisChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_storage_production_lifetime", + name="Lifetime Storage production", + api_key="eBatDisChargeTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + ), + GrowattSensorEntityDescription( + key="storage_grid_discharge_today", + name="Grid discharged today", + api_key="eacDisChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_load_consumption_today", + name="Load consumption today", + api_key="eopDischrToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_load_consumption_lifetime", + name="Lifetime load consumption", + api_key="eopDischrTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + ), + GrowattSensorEntityDescription( + key="storage_grid_charged_today", + name="Grid charged today", + api_key="eacChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_charge_storage_lifetime", + name="Lifetime storaged charged", + api_key="eChargeTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + ), + GrowattSensorEntityDescription( + key="storage_solar_production", + name="Solar power production", + api_key="ppv", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="storage_battery_percentage", + name="Battery percentage", + api_key="capacity", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + ), + GrowattSensorEntityDescription( + key="storage_power_flow", + name="Storage charging/ discharging(-ve)", + api_key="pCharge", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="storage_load_consumption_solar_storage", + name="Load consumption(Solar + Storage)", + api_key="rateVA", + native_unit_of_measurement="VA", + ), + GrowattSensorEntityDescription( + key="storage_charge_today", + name="Charge today", + api_key="eChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_import_from_grid", + name="Import from grid", + api_key="pAcInPut", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="storage_import_from_grid_today", + name="Import from grid today", + api_key="eToUserToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_import_from_grid_total", + name="Import from grid total", + api_key="eToUserTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + ), + GrowattSensorEntityDescription( + key="storage_load_consumption", + name="Load consumption", + api_key="outPutPower", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="storage_grid_voltage", + name="AC input voltage", + api_key="vGrid", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_pv_charging_voltage", + name="PV charging voltage", + api_key="vpv", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_ac_input_frequency_out", + name="AC input frequency", + api_key="freqOutPut", + native_unit_of_measurement=FREQUENCY_HERTZ, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_output_voltage", + name="Output voltage", + api_key="outPutVolt", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_ac_output_frequency", + name="Ac output frequency", + api_key="freqGrid", + native_unit_of_measurement=FREQUENCY_HERTZ, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_current_PV", + name="Solar charge current", + api_key="iAcCharge", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_current_1", + name="Solar current to storage", + api_key="iChargePV1", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_grid_amperage_input", + name="Grid charge current", + api_key="chgCurr", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_grid_out_current", + name="Grid out current", + api_key="outPutCurrent", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_battery_voltage", + name="Battery voltage", + api_key="vBat", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_load_percentage", + name="Load percentage", + api_key="loadPercent", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + precision=2, + ), +) diff --git a/homeassistant/components/growatt_server/sensor_types/tlx.py b/homeassistant/components/growatt_server/sensor_types/tlx.py new file mode 100644 index 00000000000..acffb0ac98a --- /dev/null +++ b/homeassistant/components/growatt_server/sensor_types/tlx.py @@ -0,0 +1,197 @@ +"""Growatt Sensor definitions for the TLX type.""" +from __future__ import annotations + +from homeassistant.components.sensor import ( + STATE_CLASS_TOTAL, + STATE_CLASS_TOTAL_INCREASING, +) +from homeassistant.const import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + FREQUENCY_HERTZ, + POWER_WATT, + TEMP_CELSIUS, +) + +from .sensor_entity_description import GrowattSensorEntityDescription + +TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( + GrowattSensorEntityDescription( + key="tlx_energy_today", + name="Energy today", + api_key="eacToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_energy_total", + name="Lifetime energy output", + api_key="eacTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_energy_total_input_1", + name="Lifetime total energy input 1", + api_key="epv1Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_energy_today_input_1", + name="Energy Today Input 1", + api_key="epv1Today", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_voltage_input_1", + name="Input 1 voltage", + api_key="vpv1", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_amperage_input_1", + name="Input 1 Amperage", + api_key="ipv1", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_wattage_input_1", + name="Input 1 Wattage", + api_key="ppv1", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_energy_total_input_2", + name="Lifetime total energy input 2", + api_key="epv2Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_energy_today_input_2", + name="Energy Today Input 2", + api_key="epv2Today", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_voltage_input_2", + name="Input 2 voltage", + api_key="vpv2", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_amperage_input_2", + name="Input 2 Amperage", + api_key="ipv2", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_wattage_input_2", + name="Input 2 Wattage", + api_key="ppv2", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_internal_wattage", + name="Internal wattage", + api_key="ppv", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_reactive_voltage", + name="Reactive voltage", + api_key="vacrs", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_frequency", + name="AC frequency", + api_key="fac", + native_unit_of_measurement=FREQUENCY_HERTZ, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_current_wattage", + name="Output power", + api_key="pac", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_temperature_1", + name="Temperature 1", + api_key="temp1", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_temperature_2", + name="Temperature 2", + api_key="temp2", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_temperature_3", + name="Temperature 3", + api_key="temp3", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_temperature_4", + name="Temperature 4", + api_key="temp4", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + precision=1, + ), + GrowattSensorEntityDescription( + key="tlx_temperature_5", + name="Temperature 5", + api_key="temp5", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + precision=1, + ), +) diff --git a/homeassistant/components/growatt_server/sensor_types/total.py b/homeassistant/components/growatt_server/sensor_types/total.py new file mode 100644 index 00000000000..5f5282748d1 --- /dev/null +++ b/homeassistant/components/growatt_server/sensor_types/total.py @@ -0,0 +1,56 @@ +"""Growatt Sensor definitions for Totals.""" +from __future__ import annotations + +from homeassistant.components.sensor import STATE_CLASS_TOTAL +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) + +from .sensor_entity_description import GrowattSensorEntityDescription + +TOTAL_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( + GrowattSensorEntityDescription( + key="total_money_today", + name="Total money today", + api_key="plantMoneyText", + currency=True, + ), + GrowattSensorEntityDescription( + key="total_money_total", + name="Money lifetime", + api_key="totalMoneyText", + currency=True, + ), + GrowattSensorEntityDescription( + key="total_energy_today", + name="Energy Today", + api_key="todayEnergy", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="total_output_power", + name="Output Power", + api_key="invTodayPpv", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="total_energy_output", + name="Lifetime energy output", + api_key="totalEnergy", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL, + ), + GrowattSensorEntityDescription( + key="total_maximum_output", + name="Maximum power", + api_key="nominalPower", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), +) diff --git a/homeassistant/components/growatt_server/translations/bg.json b/homeassistant/components/growatt_server/translations/bg.json new file mode 100644 index 00000000000..02c83a6e916 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 94413c76578..3610d3d3d80 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -29,7 +29,6 @@ from .const import ( DATA_COORDINATOR, DATA_COORDINATOR_PAIRED_SENSOR, DATA_PAIRED_SENSOR_MANAGER, - DATA_UNSUB_DISPATCHER_CONNECT, DOMAIN, LOGGER, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, @@ -41,22 +40,13 @@ PLATFORMS = ["binary_sensor", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elexa Guardian from a config entry.""" - hass.data.setdefault( - DOMAIN, - { - DATA_CLIENT: {}, - DATA_COORDINATOR: {}, - DATA_COORDINATOR_PAIRED_SENSOR: {}, - DATA_PAIRED_SENSOR_MANAGER: {}, - DATA_UNSUB_DISPATCHER_CONNECT: {}, - }, - ) - client = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = Client( - entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT] - ) - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} - hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id] = {} - hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id] = [] + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + DATA_COORDINATOR: {}, + DATA_COORDINATOR_PAIRED_SENSOR: {}, + } + + client = Client(entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT]) # The valve controller's UDP-based API can't handle concurrent requests very well, # so we use a lock to ensure that only one API request is reaching it at a time: @@ -71,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (API_VALVE_STATUS, client.valve.status), (API_WIFI_STATUS, client.wifi.status), ): - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ api ] = GuardianDataUpdateCoordinator( hass, @@ -84,11 +74,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: init_valve_controller_tasks.append(coordinator.async_refresh()) await asyncio.gather(*init_valve_controller_tasks) + hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] = client # Set up an object to evaluate each batch of paired sensor UIDs and add/remove # devices as appropriate: - paired_sensor_manager = hass.data[DOMAIN][DATA_PAIRED_SENSOR_MANAGER][ - entry.entry_id + paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ + DATA_PAIRED_SENSOR_MANAGER ] = PairedSensorManager(hass, entry, client, api_lock) await paired_sensor_manager.async_process_latest_paired_sensor_uids() @@ -99,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: paired_sensor_manager.async_process_latest_paired_sensor_uids() ) - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ API_SENSOR_PAIR_DUMP ].async_add_listener(async_process_paired_sensor_uids) @@ -113,12 +104,7 @@ 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][DATA_CLIENT].pop(entry.entry_id) - hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) - hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR].pop(entry.entry_id) - for unsub in hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id]: - unsub() - hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -146,8 +132,8 @@ class PairedSensorManager: self._paired_uids.add(uid) - coordinator = self._hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ - self._entry.entry_id + coordinator = self._hass.data[DOMAIN][self._entry.entry_id][ + DATA_COORDINATOR_PAIRED_SENSOR ][uid] = GuardianDataUpdateCoordinator( self._hass, client=self._client, @@ -170,7 +156,7 @@ class PairedSensorManager: """Process a list of new UIDs.""" try: uids = set( - self._hass.data[DOMAIN][DATA_COORDINATOR][self._entry.entry_id][ + self._hass.data[DOMAIN][self._entry.entry_id][DATA_COORDINATOR][ API_SENSOR_PAIR_DUMP ].data["paired_uids"] ) @@ -197,8 +183,8 @@ class PairedSensorManager: # Clear out objects related to this paired sensor: self._paired_uids.remove(uid) - self._hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ - self._entry.entry_id + self._hass.data[DOMAIN][self._entry.entry_id][ + DATA_COORDINATOR_PAIRED_SENSOR ].pop(uid) # Remove the paired sensor device from the device registry (which will @@ -217,7 +203,7 @@ class GuardianEntity(CoordinatorEntity): self, entry: ConfigEntry, description: EntityDescription ) -> None: """Initialize.""" - self._attr_device_info = {"manufacturer": "Elexa"} + self._attr_device_info = DeviceInfo(manufacturer="Elexa") self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: "Data provided by Elexa"} self._entry = entry self.entity_description = description @@ -244,11 +230,11 @@ class PairedSensorEntity(GuardianEntity): super().__init__(entry, description) paired_sensor_uid = coordinator.data["uid"] - self._attr_device_info = { - "identifiers": {(DOMAIN, paired_sensor_uid)}, - "name": f"Guardian Paired Sensor {paired_sensor_uid}", - "via_device": (DOMAIN, entry.data[CONF_UID]), - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, paired_sensor_uid)}, + name=f"Guardian Paired Sensor {paired_sensor_uid}", + via_device=(DOMAIN, entry.data[CONF_UID]), + ) self._attr_name = ( f"Guardian Paired Sensor {paired_sensor_uid}: {description.name}" ) @@ -272,11 +258,11 @@ class ValveControllerEntity(GuardianEntity): """Initialize.""" super().__init__(entry, description) - self._attr_device_info = { - "identifiers": {(DOMAIN, entry.data[CONF_UID])}, - "name": f"Guardian Valve Controller {entry.data[CONF_UID]}", - "model": coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.data[CONF_UID])}, + model=coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], + name=f"Guardian Valve Controller {entry.data[CONF_UID]}", + ) self._attr_name = f"Guardian {entry.data[CONF_UID]}: {description.name}" self._attr_unique_id = f"{entry.data[CONF_UID]}_{description.key}" self.coordinators = coordinators diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index b420605a0ec..6ac07274501 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -21,7 +22,6 @@ from .const import ( CONF_UID, DATA_COORDINATOR, DATA_COORDINATOR_PAIRED_SENSOR, - DATA_UNSUB_DISPATCHER_CONNECT, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) @@ -36,6 +36,7 @@ SENSOR_DESCRIPTION_AP_ENABLED = BinarySensorEntityDescription( key=SENSOR_KIND_AP_INFO, name="Onboard AP Enabled", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ) SENSOR_DESCRIPTION_LEAK_DETECTED = BinarySensorEntityDescription( key=SENSOR_KIND_LEAK_DETECTED, @@ -46,6 +47,7 @@ SENSOR_DESCRIPTION_MOVED = BinarySensorEntityDescription( key=SENSOR_KIND_MOVED, name="Recently Moved", device_class=DEVICE_CLASS_MOVING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ) PAIRED_SENSOR_DESCRIPTIONS = ( @@ -66,7 +68,7 @@ async def async_setup_entry( @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id][ + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR_PAIRED_SENSOR][ uid ] @@ -78,7 +80,7 @@ async def async_setup_entry( ) # Handle adding paired sensors after HASS startup: - hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id].append( + entry.async_on_unload( async_dispatcher_connect( hass, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED.format(entry.data[CONF_UID]), @@ -89,7 +91,7 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorBinarySensor | ValveControllerBinarySensor] = [ ValveControllerBinarySensor( - entry, hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id], description + entry, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], description ) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -98,8 +100,8 @@ async def async_setup_entry( sensors.extend( [ PairedSensorBinarySensor(entry, coordinator, description) - for coordinator in hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ - entry.entry_id + for coordinator in hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR_PAIRED_SENSOR ].values() for description in PAIRED_SENSOR_DESCRIPTIONS ] diff --git a/homeassistant/components/guardian/const.py b/homeassistant/components/guardian/const.py index e27e8a37047..5e3779cc447 100644 --- a/homeassistant/components/guardian/const.py +++ b/homeassistant/components/guardian/const.py @@ -18,6 +18,5 @@ DATA_CLIENT = "client" DATA_COORDINATOR = "coordinator" DATA_COORDINATOR_PAIRED_SENSOR = "coordinator_paired_sensor" DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager" -DATA_UNSUB_DISPATCHER_CONNECT = "unsub_dispatcher_connect" SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED = "guardian_paired_sensor_coordinator_added_{0}" diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 16b05e20767..9a88805baaf 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_FAHRENHEIT, TIME_MINUTES, @@ -25,7 +26,6 @@ from .const import ( CONF_UID, DATA_COORDINATOR, DATA_COORDINATOR_PAIRED_SENSOR, - DATA_UNSUB_DISPATCHER_CONNECT, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) @@ -38,6 +38,7 @@ SENSOR_DESCRIPTION_BATTERY = SensorEntityDescription( key=SENSOR_KIND_BATTERY, name="Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, ) SENSOR_DESCRIPTION_TEMPERATURE = SensorEntityDescription( @@ -51,6 +52,7 @@ SENSOR_DESCRIPTION_UPTIME = SensorEntityDescription( key=SENSOR_KIND_UPTIME, name="Uptime", icon="mdi:timer", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=TIME_MINUTES, ) @@ -72,7 +74,7 @@ async def async_setup_entry( @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id][ + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR_PAIRED_SENSOR][ uid ] @@ -84,7 +86,7 @@ async def async_setup_entry( ) # Handle adding paired sensors after HASS startup: - hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id].append( + entry.async_on_unload( async_dispatcher_connect( hass, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED.format(entry.data[CONF_UID]), @@ -95,7 +97,7 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorSensor | ValveControllerSensor] = [ ValveControllerSensor( - entry, hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id], description + entry, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], description ) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -104,8 +106,8 @@ async def async_setup_entry( sensors.extend( [ PairedSensorSensor(entry, coordinator, description) - for coordinator in hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ - entry.entry_id + for coordinator in hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR_PAIRED_SENSOR ].values() for description in PAIRED_SENSOR_DESCRIPTIONS ] diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 9b7db16dd15..7417499e53a 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -81,8 +81,8 @@ async def async_setup_entry( [ ValveControllerSwitch( entry, - hass.data[DOMAIN][DATA_CLIENT][entry.entry_id], - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id], + hass.data[DOMAIN][entry.entry_id][DATA_CLIENT], + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], ) ] ) @@ -160,8 +160,8 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): LOGGER.error("Error while adding paired sensor: %s", err) return - await self.hass.data[DOMAIN][DATA_PAIRED_SENSOR_MANAGER][ - self._entry.entry_id + await self.hass.data[DOMAIN][self._entry.entry_id][ + DATA_PAIRED_SENSOR_MANAGER ].async_pair_sensor(uid) async def async_reboot(self) -> None: @@ -189,8 +189,8 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): LOGGER.error("Error while removing paired sensor: %s", err) return - await self.hass.data[DOMAIN][DATA_PAIRED_SENSOR_MANAGER][ - self._entry.entry_id + await self.hass.data[DOMAIN][self._entry.entry_id][ + DATA_PAIRED_SENSOR_MANAGER ].async_unpair_sensor(uid) async def async_upgrade_firmware( diff --git a/homeassistant/components/guardian/translations/bg.json b/homeassistant/components/guardian/translations/bg.json new file mode 100644 index 00000000000..9c063cbbd0d --- /dev/null +++ b/homeassistant/components/guardian/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index ecd1b7de01b..04b62bb660e 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 1d1536d1679..a08932d9c1b 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -164,7 +164,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index ae27e0a51fc..cd488819eda 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -1,12 +1,13 @@ """Support for Habitica sensors.""" from collections import namedtuple from datetime import timedelta +from http import HTTPStatus import logging from aiohttp import ClientResponseError from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_NAME, HTTP_TOO_MANY_REQUESTS +from homeassistant.const import CONF_NAME from homeassistant.util import Throttle from .const import DOMAIN @@ -94,7 +95,7 @@ class HabitipyData: try: self.data = await self.api.user.get() except ClientResponseError as error: - if error.status == HTTP_TOO_MANY_REQUESTS: + if error.status == HTTPStatus.TOO_MANY_REQUESTS: _LOGGER.warning( "Sensor data update for %s has too many API requests;" " Skipping the update", @@ -111,7 +112,7 @@ class HabitipyData: try: self.tasks[task_type] = await self.api.tasks.user.get(type=task_type) except ClientResponseError as error: - if error.status == HTTP_TOO_MANY_REQUESTS: + if error.status == HTTPStatus.TOO_MANY_REQUESTS: _LOGGER.warning( "Sensor data update for %s has too many API requests;" " Skipping the update", @@ -213,8 +214,7 @@ class HabitipyTaskSensor(SensorEntity): task_id = received_task[TASKS_MAP_ID] task = {} for map_key, map_value in TASKS_MAP.items(): - value = received_task.get(map_value) - if value: + if value := received_task.get(map_value): task[map_key] = value attrs[task_id] = task return attrs diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 04814a9c3e9..820aab8cb73 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -57,8 +57,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Hangouts bot component.""" - config = config.get(DOMAIN) - if config is None: + if (config := config.get(DOMAIN)) is None: hass.data[DOMAIN] = { CONF_INTENTS: {}, CONF_DEFAULT_CONVERSATIONS: [], diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 24be9fff779..16872079be3 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -1,6 +1,7 @@ """The Hangouts Bot.""" import asyncio from contextlib import suppress +from http import HTTPStatus import io import logging @@ -8,7 +9,6 @@ import aiohttp import hangups from hangups import ChatMessageEvent, ChatMessageSegment, Client, get_auth, hangouts_pb2 -from homeassistant.const import HTTP_OK from homeassistant.core import callback from homeassistant.helpers import dispatcher, intent from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -273,7 +273,7 @@ class HangoutsBot: try: websession = async_get_clientsession(self.hass) async with websession.get(uri, timeout=5) as response: - if response.status != HTTP_OK: + if response.status != HTTPStatus.OK: _LOGGER.error( "Fetch image failed, %s, %s", response.status, response ) diff --git a/homeassistant/components/hangouts/translations/cs.json b/homeassistant/components/hangouts/translations/cs.json index 8e721ed5ff1..11bef6d1d1a 100644 --- a/homeassistant/components/hangouts/translations/cs.json +++ b/homeassistant/components/hangouts/translations/cs.json @@ -14,6 +14,7 @@ "data": { "2fa": "Dvoufaktorov\u00fd ov\u011b\u0159ovac\u00ed k\u00f3d" }, + "description": "Pr\u00e1zdn\u00e9", "title": "Dvoufaktorov\u00e9 ov\u011b\u0159en\u00ed" }, "user": { @@ -22,6 +23,7 @@ "email": "E-mail", "password": "Heslo" }, + "description": "Pr\u00e1zdn\u00e9", "title": "P\u0159ihl\u00e1\u0161en\u00ed do slu\u017eby Google Hangouts" } } diff --git a/homeassistant/components/hangouts/translations/ja.json b/homeassistant/components/hangouts/translations/ja.json new file mode 100644 index 00000000000..751e1ae41a1 --- /dev/null +++ b/homeassistant/components/hangouts/translations/ja.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_login": "\u30ed\u30b0\u30a4\u30f3\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "2fa": { + "data": { + "2fa": "2\u8981\u7d20 PIN" + }, + "description": "\u7a7a", + "title": "2\u8981\u7d20\u8a8d\u8a3c" + }, + "user": { + "data": { + "email": "Email", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "\u7a7a", + "title": "Google \u30cf\u30f3\u30b0\u30a2\u30a6\u30c8 \u30ed\u30b0\u30a4\u30f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index e2ac03e259c..4ec610f1f75 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -101,7 +101,7 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): ) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 78377265c07..706e06e881e 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -10,6 +10,7 @@ import aioharmony.exceptions as aioexc from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.entity import DeviceInfo from .const import ACTIVITY_POWER_OFF from .subscriber import HarmonySubscriberMixin @@ -82,20 +83,20 @@ class HarmonyData(HarmonySubscriberMixin): """Return the current activity tuple.""" return self._client.current_activity - def device_info(self, domain: str): + def device_info(self, domain: str) -> DeviceInfo: """Return hub device info.""" model = "Harmony Hub" if "ethernetStatus" in self._client.hub_config.info: model = "Harmony Hub Pro 2400" - return { - "identifiers": {(domain, self.unique_id)}, - "manufacturer": "Logitech", - "sw_version": self._client.hub_config.info.get( + return DeviceInfo( + identifiers={(domain, self.unique_id)}, + manufacturer="Logitech", + model=model, + name=self.name, + sw_version=self._client.hub_config.info.get( "hubSwVersion", self._client.fw_version ), - "name": self.name, - "model": model, - } + ) async def connect(self) -> bool: """Connect to the Harmony Hub.""" diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 806b638aee8..3431eff7994 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -212,8 +212,7 @@ class HarmonyRemote(HarmonyEntity, remote.RemoteEntity, RestoreEntity): if self._last_activity: activity = self._last_activity else: - all_activities = self._data.activity_names - if all_activities: + if all_activities := self._data.activity_names: activity = all_activities[0] if activity: @@ -228,8 +227,7 @@ class HarmonyRemote(HarmonyEntity, remote.RemoteEntity, RestoreEntity): async def async_send_command(self, command, **kwargs): """Send a list of commands to one device.""" _LOGGER.debug("%s: Send Command", self.name) - device = kwargs.get(ATTR_DEVICE) - if device is None: + if (device := kwargs.get(ATTR_DEVICE)) is None: _LOGGER.error("%s: Missing required argument: device", self.name) return @@ -257,8 +255,7 @@ class HarmonyRemote(HarmonyEntity, remote.RemoteEntity, RestoreEntity): _LOGGER.debug( "%s: Writing hub configuration to file: %s", self.name, self._config_path ) - json_config = self._data.json_config - if json_config is None: + if (json_config := self._data.json_config) is None: _LOGGER.warning("%s: No configuration received from hub", self.name) return diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index eacf5be5f9f..6ad1c67f1f3 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -1,6 +1,7 @@ """Support for Hass.io.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging import os @@ -16,6 +17,7 @@ from homeassistant.components.homeassistant import ( import homeassistant.config as conf_util from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_MANUFACTURER, ATTR_NAME, ATTR_SERVICE, EVENT_CORE_CONFIG_UPDATE, @@ -26,6 +28,7 @@ from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, recorder from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.loader import bind_hass @@ -43,9 +46,11 @@ from .const import ( ATTR_PASSWORD, ATTR_REPOSITORY, ATTR_SLUG, - ATTR_SNAPSHOT, + ATTR_STARTED, + ATTR_STATE, ATTR_URL, ATTR_VERSION, + DATA_KEY_ADDONS, DOMAIN, SupervisorEntityModel, ) @@ -76,7 +81,8 @@ DATA_STORE = "hassio_store" DATA_INFO = "hassio_info" DATA_OS_INFO = "hassio_os_info" DATA_SUPERVISOR_INFO = "hassio_supervisor_info" -HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) +DATA_ADDONS_STATS = "hassio_addons_stats" +HASSIO_UPDATE_INTERVAL = timedelta(minutes=5) ADDONS_COORDINATOR = "hassio_addons_coordinator" @@ -87,8 +93,6 @@ SERVICE_ADDON_UPDATE = "addon_update" SERVICE_ADDON_STDIN = "addon_stdin" SERVICE_HOST_SHUTDOWN = "host_shutdown" SERVICE_HOST_REBOOT = "host_reboot" -SERVICE_SNAPSHOT_FULL = "snapshot_full" -SERVICE_SNAPSHOT_PARTIAL = "snapshot_partial" SERVICE_BACKUP_FULL = "backup_full" SERVICE_BACKUP_PARTIAL = "backup_partial" SERVICE_RESTORE_FULL = "restore_full" @@ -116,11 +120,9 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( SCHEMA_RESTORE_FULL = vol.Schema( { - vol.Exclusive(ATTR_SLUG, ATTR_SLUG): cv.slug, - vol.Exclusive(ATTR_SNAPSHOT, ATTR_SLUG): cv.slug, + vol.Required(ATTR_SLUG): cv.slug, vol.Optional(ATTR_PASSWORD): cv.string, - }, - cv.has_at_least_one_key(ATTR_SLUG, ATTR_SNAPSHOT), + } ) SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend( @@ -175,18 +177,6 @@ MAP_SERVICE_API = { None, True, ), - SERVICE_SNAPSHOT_FULL: APIEndpointSettings( - "/backups/new/full", - SCHEMA_BACKUP_FULL, - None, - True, - ), - SERVICE_SNAPSHOT_PARTIAL: APIEndpointSettings( - "/backups/new/partial", - SCHEMA_BACKUP_PARTIAL, - None, - True, - ), } @@ -360,6 +350,16 @@ def get_supervisor_info(hass): return hass.data.get(DATA_SUPERVISOR_INFO) +@callback +@bind_hass +def get_addons_stats(hass): + """Return Addons stats. + + Async friendly. + """ + return hass.data.get(DATA_ADDONS_STATS) + + @callback @bind_hass def get_os_info(hass): @@ -489,22 +489,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: """Handle service calls for Hass.io.""" api_endpoint = MAP_SERVICE_API[service.service] - if "snapshot" in service.service: - _LOGGER.warning( - "The service '%s' is deprecated and will be removed in Home Assistant 2021.11, use '%s' instead", - service.service, - service.service.replace("snapshot", "backup"), - ) data = service.data.copy() addon = data.pop(ATTR_ADDON, None) slug = data.pop(ATTR_SLUG, None) - snapshot = data.pop(ATTR_SNAPSHOT, None) - if snapshot is not None: - _LOGGER.warning( - "Using 'snapshot' is deprecated and will be removed in Home Assistant 2021.11, use 'slug' instead" - ) - slug = snapshot - payload = None # Pass data to Hass.io API @@ -529,25 +516,51 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: DOMAIN, service, async_service_handler, schema=settings.schema ) + async def update_addon_stats(slug): + """Update single addon stats.""" + stats = await hassio.get_addon_stats(slug) + return (slug, stats) + async def update_info_data(now): """Update last available supervisor information.""" + try: - hass.data[DATA_INFO] = await hassio.get_info() - hass.data[DATA_HOST_INFO] = await hassio.get_host_info() - hass.data[DATA_STORE] = await hassio.get_store() - hass.data[DATA_CORE_INFO] = await hassio.get_core_info() - hass.data[DATA_SUPERVISOR_INFO] = await hassio.get_supervisor_info() - hass.data[DATA_OS_INFO] = await hassio.get_os_info() + ( + hass.data[DATA_INFO], + hass.data[DATA_HOST_INFO], + hass.data[DATA_STORE], + hass.data[DATA_CORE_INFO], + hass.data[DATA_SUPERVISOR_INFO], + hass.data[DATA_OS_INFO], + ) = await asyncio.gather( + hassio.get_info(), + hassio.get_host_info(), + hassio.get_store(), + hassio.get_core_info(), + hassio.get_supervisor_info(), + hassio.get_os_info(), + ) + + addons = [ + addon + for addon in hass.data[DATA_SUPERVISOR_INFO].get("addons", []) + if addon[ATTR_STATE] == ATTR_STARTED + ] + stats_data = await asyncio.gather( + *[update_addon_stats(addon[ATTR_SLUG]) for addon in addons] + ) + hass.data[DATA_ADDONS_STATS] = dict(stats_data) + if ADDONS_COORDINATOR in hass.data: await hass.data[ADDONS_COORDINATOR].async_refresh() except HassioAPIError as err: - _LOGGER.warning("Can't read last version: %s", err) + _LOGGER.warning("Can't read Supervisor data: %s", err) hass.helpers.event.async_track_point_in_utc_time( update_info_data, utcnow() + HASSIO_UPDATE_INTERVAL ) - # Fetch last version + # Fetch data await update_info_data(None) async def async_handle_core_service(call): @@ -645,17 +658,17 @@ def async_register_addons_in_dev_reg( ) -> None: """Register addons in the device registry.""" for addon in addons: - params = { - "config_entry_id": entry_id, - "identifiers": {(DOMAIN, addon[ATTR_SLUG])}, - "model": SupervisorEntityModel.ADDON, - "sw_version": addon[ATTR_VERSION], - "name": addon[ATTR_NAME], - "entry_type": ATTR_SERVICE, - } + params = DeviceInfo( + identifiers={(DOMAIN, addon[ATTR_SLUG])}, + model=SupervisorEntityModel.ADDON, + sw_version=addon[ATTR_VERSION], + name=addon[ATTR_NAME], + entry_type=ATTR_SERVICE, + configuration_url=f"homeassistant://hassio/addon/{addon[ATTR_SLUG]}", + ) if manufacturer := addon.get(ATTR_REPOSITORY) or addon.get(ATTR_URL): - params["manufacturer"] = manufacturer - dev_reg.async_get_or_create(**params) + params[ATTR_MANUFACTURER] = manufacturer + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) @callback @@ -663,16 +676,15 @@ def async_register_os_in_dev_reg( entry_id: str, dev_reg: DeviceRegistry, os_dict: dict[str, Any] ) -> None: """Register OS in the device registry.""" - params = { - "config_entry_id": entry_id, - "identifiers": {(DOMAIN, "OS")}, - "manufacturer": "Home Assistant", - "model": SupervisorEntityModel.OS, - "sw_version": os_dict[ATTR_VERSION], - "name": "Home Assistant Operating System", - "entry_type": ATTR_SERVICE, - } - dev_reg.async_get_or_create(**params) + params = DeviceInfo( + identifiers={(DOMAIN, "OS")}, + manufacturer="Home Assistant", + model=SupervisorEntityModel.OS, + sw_version=os_dict[ATTR_VERSION], + name="Home Assistant Operating System", + entry_type=ATTR_SERVICE, + ) + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) @callback @@ -705,6 +717,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): """Update data via library.""" new_data = {} supervisor_info = get_supervisor_info(self.hass) + addons_stats = get_addons_stats(self.hass) store_data = get_store(self.hass) repositories = { @@ -712,9 +725,10 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): for repo in store_data.get("repositories", []) } - new_data["addons"] = { + new_data[DATA_KEY_ADDONS] = { addon[ATTR_SLUG]: { **addon, + **((addons_stats or {}).get(addon[ATTR_SLUG], {})), ATTR_REPOSITORY: repositories.get( addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "") ), @@ -727,7 +741,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): # If this is the initial refresh, register all addons and return the dict if not self.data: async_register_addons_in_dev_reg( - self.entry_id, self.dev_reg, new_data["addons"].values() + self.entry_id, self.dev_reg, new_data[DATA_KEY_ADDONS].values() ) if self.is_hass_os: async_register_os_in_dev_reg( @@ -741,13 +755,15 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): if self.entry_id in device.config_entries and device.model == SupervisorEntityModel.ADDON } - if stale_addons := supervisor_addon_devices - set(new_data["addons"]): + if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_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 self.data and set(new_data["addons"]) - set(self.data["addons"]): + if self.data and set(new_data[DATA_KEY_ADDONS]) - set( + self.data[DATA_KEY_ADDONS] + ): self.hass.async_create_task( self.hass.config_entries.async_reload(self.entry_id) ) diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index d540479d779..d6240896c84 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -1,11 +1,12 @@ """Implement the Ingress Panel feature for Hass.io Add-ons.""" import asyncio +from http import HTTPStatus import logging from aiohttp import web from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ATTR_ICON, HTTP_BAD_REQUEST +from homeassistant.const import ATTR_ICON from homeassistant.core import HomeAssistant from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_PANELS, ATTR_TITLE @@ -53,7 +54,7 @@ class HassIOAddonPanel(HomeAssistantView): # Panel exists for add-on slug if addon not in panels or not panels[addon][ATTR_ENABLE]: _LOGGER.error("Panel is not enable for %s", addon) - return web.Response(status=HTTP_BAD_REQUEST) + return web.Response(status=HTTPStatus.BAD_REQUEST) data = panels[addon] # Register panel diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 78925ed73fc..2d76a758096 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -1,4 +1,5 @@ """Implement the auth feature from Hass.io for Add-ons.""" +from http import HTTPStatus from ipaddress import ip_address import logging import os @@ -12,7 +13,6 @@ from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import HTTP_OK from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -83,7 +83,7 @@ class HassIOAuth(HassIOBaseAuth): except auth_ha.InvalidAuth: raise HTTPNotFound() from None - return web.Response(status=HTTP_OK) + return web.Response(status=HTTPStatus.OK) class HassIOPasswordReset(HassIOBaseAuth): @@ -113,4 +113,4 @@ class HassIOPasswordReset(HassIOBaseAuth): except auth_ha.InvalidUser as err: raise HTTPNotFound() from err - return web.Response(status=HTTP_OK) + return web.Response(status=HTTPStatus.OK) diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index dfd13adbde6..5078e11c26e 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -1,7 +1,10 @@ """Binary sensor platform for Hass.io addons.""" from __future__ import annotations +from dataclasses import dataclass + from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_RUNNING, DEVICE_CLASS_UPDATE, BinarySensorEntity, BinarySensorEntityDescription, @@ -11,16 +14,37 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ADDONS_COORDINATOR -from .const import ATTR_UPDATE_AVAILABLE, DATA_KEY_ADDONS, DATA_KEY_OS +from .const import ( + ATTR_STARTED, + ATTR_STATE, + ATTR_UPDATE_AVAILABLE, + DATA_KEY_ADDONS, + DATA_KEY_OS, +) from .entity import HassioAddonEntity, HassioOSEntity + +@dataclass +class HassioBinarySensorEntityDescription(BinarySensorEntityDescription): + """Hassio binary sensor entity description.""" + + target: str | None = None + + ENTITY_DESCRIPTIONS = ( - BinarySensorEntityDescription( + HassioBinarySensorEntityDescription( device_class=DEVICE_CLASS_UPDATE, entity_registry_enabled_default=False, key=ATTR_UPDATE_AVAILABLE, name="Update Available", ), + HassioBinarySensorEntityDescription( + device_class=DEVICE_CLASS_RUNNING, + entity_registry_enabled_default=False, + key=ATTR_STATE, + name="Running", + target=ATTR_STARTED, + ), ) @@ -56,14 +80,19 @@ async def async_setup_entry( class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity): - """Binary sensor to track whether an update is available for a Hass.io add-on.""" + """Binary sensor for Hass.io add-ons.""" + + entity_description: HassioBinarySensorEntityDescription @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ + value = self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ self.entity_description.key ] + if self.entity_description.target is None: + return value + return value == self.entity_description.target class HassioOSBinarySensor(HassioOSEntity, BinarySensorEntity): diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 134fba15f70..7cdc87708ae 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -15,7 +15,6 @@ ATTR_HOMEASSISTANT = "homeassistant" ATTR_INPUT = "input" ATTR_PANELS = "panels" ATTR_PASSWORD = "password" -ATTR_SNAPSHOT = "snapshot" ATTR_TITLE = "title" ATTR_USERNAME = "username" ATTR_UUID = "uuid" @@ -43,7 +42,11 @@ EVENT_SUPERVISOR_EVENT = "supervisor_event" ATTR_VERSION = "version" ATTR_VERSION_LATEST = "version_latest" ATTR_UPDATE_AVAILABLE = "update_available" +ATTR_CPU_PERCENT = "cpu_percent" +ATTR_MEMORY_PERCENT = "memory_percent" ATTR_SLUG = "slug" +ATTR_STATE = "state" +ATTR_STARTED = "started" ATTR_URL = "url" ATTR_REPOSITORY = "repository" diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index 4a342e9965f..5dd41166c32 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -4,11 +4,11 @@ from __future__ import annotations from typing import Any from homeassistant.const import ATTR_NAME -from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import DOMAIN, HassioDataUpdateCoordinator -from .const import ATTR_SLUG +from .const import ATTR_SLUG, DATA_KEY_ADDONS, DATA_KEY_OS class HassioAddonEntity(CoordinatorEntity): @@ -26,7 +26,16 @@ class HassioAddonEntity(CoordinatorEntity): self._addon_slug = addon[ATTR_SLUG] self._attr_name = f"{addon[ATTR_NAME]}: {entity_description.name}" self._attr_unique_id = f"{addon[ATTR_SLUG]}_{entity_description.key}" - self._attr_device_info = {"identifiers": {(DOMAIN, addon[ATTR_SLUG])}} + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, addon[ATTR_SLUG])}) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and self.entity_description.key + in self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug] + ) class HassioOSEntity(CoordinatorEntity): @@ -42,4 +51,12 @@ class HassioOSEntity(CoordinatorEntity): self.entity_description = entity_description self._attr_name = f"Home Assistant Operating System: {entity_description.name}" self._attr_unique_id = f"home_assistant_os_{entity_description.key}" - self._attr_device_info = {"identifiers": {(DOMAIN, "OS")}} + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "OS")}) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and self.entity_description.key in self.coordinator.data[DATA_KEY_OS] + ) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 37b645eb7d3..4a0312bcecb 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -1,5 +1,6 @@ """Handler for Hass.io.""" import asyncio +from http import HTTPStatus import logging import os @@ -10,7 +11,7 @@ from homeassistant.components.http import ( CONF_SERVER_PORT, CONF_SSL_CERTIFICATE, ) -from homeassistant.const import HTTP_BAD_REQUEST, HTTP_OK, SERVER_PORT +from homeassistant.const import SERVER_PORT from .const import X_HASSIO @@ -118,6 +119,14 @@ class HassIO: """ return self.send_command(f"/addons/{addon}/info", method="get") + @api_data + def get_addon_stats(self, addon): + """Return stats for an Add-on. + + This method returns a coroutine. + """ + return self.send_command(f"/addons/{addon}/stats", method="get") + @api_data def get_store(self): """Return data from the store. @@ -217,7 +226,7 @@ class HassIO: timeout=aiohttp.ClientTimeout(total=timeout), ) - if request.status not in (HTTP_OK, HTTP_BAD_REQUEST): + if request.status not in (HTTPStatus.OK, HTTPStatus.BAD_REQUEST): _LOGGER.error("%s return code %d", command, request.status) raise HassioAPIError() diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index fe01cbe3197..532b947ac49 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from http import HTTPStatus import logging import os import re @@ -20,7 +21,6 @@ from aiohttp.web_exceptions import HTTPBadGateway from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.onboarding import async_is_onboarded -from homeassistant.const import HTTP_UNAUTHORIZED from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO @@ -38,15 +38,10 @@ NO_TIMEOUT = re.compile( r"|backups/.+/full" r"|backups/.+/partial" r"|backups/[^/]+/(?:upload|download)" - r"|snapshots/.+/full" - r"|snapshots/.+/partial" - r"|snapshots/[^/]+/(?:upload|download)" r")$" ) -NO_AUTH_ONBOARDING = re.compile( - r"^(?:" r"|supervisor/logs" r"|backups/[^/]+/.+" r"|snapshots/[^/]+/.+" r")$" -) +NO_AUTH_ONBOARDING = re.compile(r"^(?:" r"|supervisor/logs" r"|backups/[^/]+/.+" r")$") NO_AUTH = re.compile( r"^(?:" r"|app/.*" r"|addons/[^/]+/logo" r"|addons/[^/]+/icon" r")$" @@ -73,7 +68,7 @@ class HassIOView(HomeAssistantView): """Route data to Hass.io.""" hass = request.app["hass"] if _need_auth(hass, path) and not request[KEY_AUTHENTICATED]: - return web.Response(status=HTTP_UNAUTHORIZED) + return web.Response(status=HTTPStatus.UNAUTHORIZED) return await self._command_proxy(path, request) @@ -89,7 +84,7 @@ class HassIOView(HomeAssistantView): This method is a coroutine. """ headers = _init_header(request) - if path in ("snapshots/new/upload", "backups/new/upload"): + if path == "backups/new/upload": # We need to reuse the full content type that includes the boundary headers[ "Content-Type" @@ -134,8 +129,7 @@ def _init_header(request: web.Request) -> dict[str, str]: } # Add user data - user = request.get("hass_user") - if user is not None: + if request.get("hass_user") is not None: headers[X_HASS_USER_ID] = request["hass_user"].id headers[X_HASS_IS_ADMIN] = str(int(request["hass_user"].is_admin)) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index e58c2d790f2..620c69f543d 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -197,8 +197,7 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st headers[hdrs.X_FORWARDED_FOR] = forward_for # Set X-Forwarded-Host - forward_host = request.headers.get(hdrs.X_FORWARDED_HOST) - if not forward_host: + if not (forward_host := request.headers.get(hdrs.X_FORWARDED_HOST)): forward_host = request.host headers[hdrs.X_FORWARDED_HOST] = forward_host diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index aaa5b3669ad..27cc1eaf735 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -1,7 +1,7 @@ { "domain": "hassio", "name": "Home Assistant Supervisor", - "documentation": "https://www.home-assistant.io/hassio", + "documentation": "https://www.home-assistant.io/integrations/hassio", "dependencies": ["http"], "after_dependencies": ["panel_custom"], "codeowners": ["@home-assistant/supervisor"], diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index 55678eb29c4..0608a9f817b 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -1,16 +1,28 @@ """Sensor platform for Hass.io addons.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ADDONS_COORDINATOR -from .const import ATTR_VERSION, ATTR_VERSION_LATEST, DATA_KEY_ADDONS, DATA_KEY_OS +from .const import ( + ATTR_CPU_PERCENT, + ATTR_MEMORY_PERCENT, + ATTR_VERSION, + ATTR_VERSION_LATEST, + DATA_KEY_ADDONS, + DATA_KEY_OS, +) from .entity import HassioAddonEntity, HassioOSEntity -ENTITY_DESCRIPTIONS = ( +COMMON_ENTITY_DESCRIPTIONS = ( SensorEntityDescription( entity_registry_enabled_default=False, key=ATTR_VERSION, @@ -23,6 +35,27 @@ ENTITY_DESCRIPTIONS = ( ), ) +ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( + SensorEntityDescription( + entity_registry_enabled_default=False, + key=ATTR_CPU_PERCENT, + name="CPU Percent", + icon="mdi:cpu-64-bit", + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + entity_registry_enabled_default=False, + key=ATTR_MEMORY_PERCENT, + name="Memory Percent", + icon="mdi:memory", + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), +) + +OS_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + async def async_setup_entry( hass: HomeAssistant, @@ -34,8 +67,8 @@ async def async_setup_entry( entities = [] - for entity_description in ENTITY_DESCRIPTIONS: - for addon in coordinator.data[DATA_KEY_ADDONS].values(): + for addon in coordinator.data[DATA_KEY_ADDONS].values(): + for entity_description in ADDON_ENTITY_DESCRIPTIONS: entities.append( HassioAddonSensor( addon=addon, @@ -44,7 +77,8 @@ async def async_setup_entry( ) ) - if coordinator.is_hass_os: + if coordinator.is_hass_os: + for entity_description in OS_ENTITY_DESCRIPTIONS: entities.append( HassioOSSensor( coordinator=coordinator, diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index 3e5736c3593..6b77a180c09 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -66,52 +66,6 @@ host_shutdown: name: Poweroff the host system. description: Poweroff the host system. -snapshot_full: - name: Create a full backup. - description: Create a full backup (deprecated, use backup_full instead). - fields: - name: - name: Name - description: Optional (default = current date and time). - example: "Backup 1" - selector: - text: - password: - name: Password - description: Optional password. - example: "password" - selector: - text: - -snapshot_partial: - name: Create a partial backup. - description: Create a partial backup (deprecated, use backup_partial instead). - fields: - addons: - name: Add-ons - description: Optional list of add-on slugs. - example: ["core_ssh", "core_samba", "core_mosquitto"] - selector: - object: - folders: - name: Folders - description: Optional list of directories. - example: ["homeassistant", "share"] - selector: - object: - name: - name: Name - description: Optional (default = current date and time). - example: "Partial backup 1" - selector: - text: - password: - name: Password - description: Optional password. - example: "password" - selector: - text: - backup_full: name: Create a full backup. description: Create a full backup. @@ -164,6 +118,7 @@ restore_full: fields: slug: name: Slug + required: true description: Slug of backup to restore from. selector: text: @@ -178,6 +133,12 @@ restore_partial: name: Restore from partial backup. description: Restore from partial backup. fields: + slug: + name: Slug + required: true + description: Slug of backup to restore from. + selector: + text: homeassistant: name: Home Assistant settings description: Restore Home Assistant @@ -195,3 +156,9 @@ restore_partial: example: ["core_ssh", "core_samba", "core_mosquitto"] selector: object: + password: + name: Password + description: Optional password. + example: "password" + selector: + text: diff --git a/homeassistant/components/hassio/translations/bg.json b/homeassistant/components/hassio/translations/bg.json new file mode 100644 index 00000000000..960dc53b5ea --- /dev/null +++ b/homeassistant/components/hassio/translations/bg.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "disk_total": "\u0414\u0438\u0441\u043a \u043e\u0431\u0449\u043e", + "disk_used": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d \u0434\u0438\u0441\u043a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 738837989b9..cdcc526c8d8 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -1,5 +1,6 @@ """Support for haveibeenpwned (email breaches) sensor.""" from datetime import timedelta +from http import HTTPStatus import logging from aiohttp.hdrs import USER_AGENT @@ -7,13 +8,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_API_KEY, - CONF_EMAIL, - HTTP_NOT_FOUND, - HTTP_OK, -) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_EMAIL import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time from homeassistant.util import Throttle @@ -163,7 +158,7 @@ class HaveIBeenPwnedData: _LOGGER.error("Failed fetching data for %s", self._email) return - if req.status_code == HTTP_OK: + if req.status_code == HTTPStatus.OK: self.data[self._email] = sorted( req.json(), key=lambda k: k["AddedDate"], reverse=True ) @@ -172,7 +167,7 @@ class HaveIBeenPwnedData: # the forced updates try this current email again self.set_next_email() - elif req.status_code == HTTP_NOT_FOUND: + elif req.status_code == HTTPStatus.NOT_FOUND: self.data[self._email] = [] # only goto next email if we had data so that diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 87391634251..9dfd68d4a4f 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -1,9 +1,10 @@ """Support for HDMI CEC.""" from __future__ import annotations -from functools import partial, reduce +from functools import reduce import logging import multiprocessing +from typing import Any from pycec.cec import CecAdapter from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand @@ -41,7 +42,7 @@ from homeassistant.const import ( STATE_PLAYING, STATE_UNAVAILABLE, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import discovery, event import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -222,9 +223,12 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 hass.bus.fire(EVENT_HDMI_CEC_UNAVAILABLE) adapter.init() - hdmi_network.set_initialized_callback( - partial(event.async_call_later, hass, WATCHDOG_INTERVAL, _adapter_watchdog) - ) + @callback + def _async_initialized_callback(*_: Any): + """Add watchdog on initialization.""" + return event.async_call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog) + + hdmi_network.set_initialized_callback(_async_initialized_callback) def _volume(call): """Increase/decrease volume and mute/unmute system.""" @@ -297,8 +301,7 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 def _select_device(call): """Select the active device.""" - addr = call.data[ATTR_DEVICE] - if not addr: + if not (addr := call.data[ATTR_DEVICE]): _LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE]) return if addr in device_aliases: diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 35520927e97..a4cdb6cdddc 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -130,7 +130,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.""" controller_manager = hass.data[DOMAIN][DATA_CONTROLLER_MANAGER] await controller_manager.disconnect() diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 46a751983e9..a562d8e3d7a 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -255,13 +255,13 @@ class HeosMediaPlayer(MediaPlayerEntity): @property def device_info(self) -> DeviceInfo: """Get attributes about the device.""" - return { - "identifiers": {(HEOS_DOMAIN, self._player.player_id)}, - "name": self._player.name, - "model": self._player.model, - "manufacturer": "HEOS", - "sw_version": self._player.version, - } + return DeviceInfo( + identifiers={(HEOS_DOMAIN, self._player.player_id)}, + manufacturer="HEOS", + model=self._player.model, + name=self._player.name, + sw_version=self._player.version, + ) @property def extra_state_attributes(self) -> dict: diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 7606a2772d6..1d47bbaf89f 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -326,9 +326,7 @@ class HERETravelTimeSensor(SensorEntity): async def _get_location_from_entity(self, entity_id: str) -> str | None: """Get the location from the entity state or attributes.""" - entity = self.hass.states.get(entity_id) - - if entity is None: + if (entity := self.hass.states.get(entity_id)) is None: _LOGGER.error("Unable to find entity %s", entity_id) return None @@ -416,11 +414,9 @@ class HERETravelTimeData: # Convert location to HERE friendly location destination = self.destination.split(",") origin = self.origin.split(",") - arrival = self.arrival - if arrival is not None: + if (arrival := self.arrival) is not None: arrival = convert_time_to_isodate(arrival) - departure = self.departure - if departure is not None: + if (departure := self.departure) is not None: departure = convert_time_to_isodate(departure) if departure is None and arrival is None: @@ -486,8 +482,7 @@ class HERETravelTimeData: if suppliers is not None: supplier_titles = [] for supplier in suppliers: - title = supplier.get("title") - if title is not None: + if (title := supplier.get("title")) is not None: supplier_titles.append(title) joined_supplier_titles = ",".join(supplier_titles) attribution = f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind." diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index 9676870ecc4..a8f89401148 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -2,7 +2,7 @@ "domain": "hikvision", "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvision", - "requirements": ["pyhik==0.2.8"], + "requirements": ["pyhik==0.3.0"], "codeowners": ["@mezz64"], "iot_class": "local_push" } diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index 2f1f89cd261..aa4a430e72e 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -73,11 +73,6 @@ class HikvisionMotionSwitch(SwitchEntity): """Return the name of the device if any.""" return self._name - @property - def state(self): - """Return the state of the device if any.""" - return self._state - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py index 23a3a0c1416..096b39bdbca 100644 --- a/homeassistant/components/hisense_aehw4a1/climate.py +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -325,8 +325,7 @@ class ClimateAehW4a1(ClimateEntity): "AC at %s is off, could not set temperature", self._unique_id ) return - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is not None: + if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: _LOGGER.debug("Setting temp of %s to %s", self._unique_id, temp) if self._preset_mode != PRESET_NONE: await self.async_set_preset_mode(PRESET_NONE) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 7c3087d471f..2ac9a77c025 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -211,8 +211,7 @@ class HistoryPeriodView(HomeAssistantView): if start_time > now: return self.json([]) - end_time_str = request.query.get("end_time") - if end_time_str: + if end_time_str := request.query.get("end_time"): end_time = dt_util.parse_datetime(end_time_str) if end_time: end_time = dt_util.as_utc(end_time) @@ -304,13 +303,11 @@ class HistoryPeriodView(HomeAssistantView): def sqlalchemy_filter_from_include_exclude_conf(conf): """Build a sql filter from config.""" filters = Filters() - exclude = conf.get(CONF_EXCLUDE) - if exclude: + if exclude := conf.get(CONF_EXCLUDE): filters.excluded_entities = exclude.get(CONF_ENTITIES, []) filters.excluded_domains = exclude.get(CONF_DOMAINS, []) filters.excluded_entity_globs = exclude.get(CONF_ENTITY_GLOBS, []) - include = conf.get(CONF_INCLUDE) - if include: + if include := conf.get(CONF_INCLUDE): filters.included_entities = include.get(CONF_ENTITIES, []) filters.included_domains = include.get(CONF_DOMAINS, []) filters.included_entity_globs = include.get(CONF_ENTITY_GLOBS, []) diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index 666f6796d4c..ac362f173e4 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -1,5 +1,6 @@ """Support for the Hitron CODA-4582U, provided by Rogers.""" from collections import namedtuple +from http import HTTPStatus import logging import requests @@ -10,13 +11,7 @@ from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_TYPE, - CONF_USERNAME, - HTTP_OK, -) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TYPE, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -88,7 +83,7 @@ class HitronCODADeviceScanner(DeviceScanner): except requests.exceptions.Timeout: _LOGGER.error("Connection to the router timed out at URL %s", self._url) return False - if res.status_code != HTTP_OK: + if res.status_code != HTTPStatus.OK: _LOGGER.error("Connection failed with http code %s", res.status_code) return False try: @@ -113,7 +108,7 @@ class HitronCODADeviceScanner(DeviceScanner): except requests.exceptions.Timeout: _LOGGER.error("Connection to the router timed out at URL %s", self._url) return False - if res.status_code != HTTP_OK: + if res.status_code != HTTPStatus.OK: _LOGGER.error("Connection failed with http code %s", res.status_code) return False try: diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index d5f1ca53afd..cd1bef406d2 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SOUND, BinarySensorEntity, ) +from homeassistant.helpers.entity import DeviceInfo from . import HiveEntity from .const import ATTR_MODE, DOMAIN @@ -46,16 +47,16 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self.device["device_id"])}, - "name": self.device["device_name"], - "model": self.device["deviceData"]["model"], - "manufacturer": self.device["deviceData"]["manufacturer"], - "sw_version": self.device["deviceData"]["version"], - "via_device": (DOMAIN, self.device["parentDevice"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + manufacturer=self.device["deviceData"]["manufacturer"], + model=self.device["deviceData"]["model"], + name=self.device["device_name"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) @property def device_class(self): diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 80a6bb0941e..0b038bbde0d 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -19,6 +19,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import DeviceInfo from . import HiveEntity, refresh_system from .const import ( @@ -117,16 +118,16 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self.device["device_id"])}, - "name": self.device["device_name"], - "model": self.device["deviceData"]["model"], - "manufacturer": self.device["deviceData"]["manufacturer"], - "sw_version": self.device["deviceData"]["version"], - "via_device": (DOMAIN, self.device["parentDevice"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + manufacturer=self.device["deviceData"]["manufacturer"], + model=self.device["deviceData"]["model"], + name=self.device["device_name"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) @property def supported_features(self): diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 46e8c5b5790..f9d235d625f 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -10,6 +10,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, LightEntity, ) +from homeassistant.helpers.entity import DeviceInfo import homeassistant.util.color as color_util from . import HiveEntity, refresh_system @@ -40,16 +41,16 @@ class HiveDeviceLight(HiveEntity, LightEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self.device["device_id"])}, - "name": self.device["device_name"], - "model": self.device["deviceData"]["model"], - "manufacturer": self.device["deviceData"]["manufacturer"], - "sw_version": self.device["deviceData"]["version"], - "via_device": (DOMAIN, self.device["parentDevice"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + manufacturer=self.device["deviceData"]["manufacturer"], + model=self.device["deviceData"]["model"], + name=self.device["device_name"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) @property def name(self): diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 5ea81bff123..bca144cd59c 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity +from homeassistant.helpers.entity import DeviceInfo from . import HiveEntity from .const import DOMAIN @@ -35,16 +36,16 @@ class HiveSensorEntity(HiveEntity, SensorEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self.device["device_id"])}, - "name": self.device["device_name"], - "model": self.device["deviceData"]["model"], - "manufacturer": self.device["deviceData"]["manufacturer"], - "sw_version": self.device["deviceData"]["version"], - "via_device": (DOMAIN, self.device["parentDevice"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + manufacturer=self.device["deviceData"]["manufacturer"], + model=self.device["deviceData"]["model"], + name=self.device["device_name"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) @property def available(self): diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 7ad81a25f0e..adfcfa442ee 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,7 +1,10 @@ """Support for the Hive switches.""" +from __future__ import annotations + from datetime import timedelta from homeassistant.components.switch import SwitchEntity +from homeassistant.helpers.entity import DeviceInfo from . import HiveEntity, refresh_system from .const import ATTR_MODE, DOMAIN @@ -31,17 +34,18 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return device information.""" if self.device["hiveType"] == "activeplug": - return { - "identifiers": {(DOMAIN, self.device["device_id"])}, - "name": self.device["device_name"], - "model": self.device["deviceData"]["model"], - "manufacturer": self.device["deviceData"]["manufacturer"], - "sw_version": self.device["deviceData"]["version"], - "via_device": (DOMAIN, self.device["parentDevice"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + manufacturer=self.device["deviceData"]["manufacturer"], + model=self.device["deviceData"]["model"], + name=self.device["device_name"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) + return None @property def name(self): diff --git a/homeassistant/components/hive/translations/hu.json b/homeassistant/components/hive/translations/hu.json index 469b99debe1..9b0d3c21590 100644 --- a/homeassistant/components/hive/translations/hu.json +++ b/homeassistant/components/hive/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unknown_entry": "Nem tal\u00e1lhat\u00f3 megl\u00e9v\u0151 bejegyz\u00e9s." }, "error": { diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index b9377a378c3..096fc468ecb 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -13,6 +13,7 @@ from homeassistant.components.water_heater import ( ) from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import DeviceInfo from . import HiveEntity, refresh_system from .const import ( @@ -78,16 +79,16 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self.device["device_id"])}, - "name": self.device["device_name"], - "model": self.device["deviceData"]["model"], - "manufacturer": self.device["deviceData"]["manufacturer"], - "sw_version": self.device["deviceData"]["version"], - "via_device": (DOMAIN, self.device["parentDevice"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + manufacturer=self.device["deviceData"]["manufacturer"], + model=self.device["deviceData"]["model"], + name=self.device["device_name"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) @property def supported_features(self): diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py index 12f86059023..b27988f997d 100644 --- a/homeassistant/components/home_connect/entity.py +++ b/homeassistant/components/home_connect/entity.py @@ -4,7 +4,7 @@ import logging from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .api import HomeConnectDevice from .const import DOMAIN, SIGNAL_UPDATE_ENTITIES @@ -51,14 +51,14 @@ class HomeConnectEntity(Entity): return f"{self.device.appliance.haId}-{self.desc}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return info about the device.""" - return { - "identifiers": {(DOMAIN, self.device.appliance.haId)}, - "name": self.device.appliance.name, - "manufacturer": self.device.appliance.brand, - "model": self.device.appliance.vib, - } + return DeviceInfo( + identifiers={(DOMAIN, self.device.appliance.haId)}, + manufacturer=self.device.appliance.brand, + model=self.device.appliance.vib, + name=self.device.appliance.name, + ) @callback def async_entity_update(self): diff --git a/homeassistant/components/home_connect/translations/bg.json b/homeassistant/components/home_connect/translations/bg.json new file mode 100644 index 00000000000..ac264191dcd --- /dev/null +++ b/homeassistant/components/home_connect/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d" + }, + "step": { + "pick_implementation": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/switch.py b/homeassistant/components/home_plus_control/switch.py index d4167ae1f9e..809a246e631 100644 --- a/homeassistant/components/home_plus_control/switch.py +++ b/homeassistant/components/home_plus_control/switch.py @@ -8,6 +8,7 @@ from homeassistant.components.switch import ( ) from homeassistant.core import callback from homeassistant.helpers import dispatcher +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DISPATCHER_REMOVERS, DOMAIN, HW_TYPE, SIGNAL_ADD_ENTITIES @@ -79,18 +80,18 @@ class HomeControlSwitchEntity(CoordinatorEntity, SwitchEntity): return self.idx @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device information.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Unique identifiers within the domain (DOMAIN, self.unique_id) }, - "name": self.name, - "manufacturer": "Legrand", - "model": HW_TYPE.get(self.module.hw_type), - "sw_version": self.module.fw, - } + manufacturer="Legrand", + model=HW_TYPE.get(self.module.hw_type), + name=self.name, + sw_version=self.module.fw, + ) @property def device_class(self): diff --git a/homeassistant/components/home_plus_control/translations/hu.json b/homeassistant/components/home_plus_control/translations/hu.json index 2dc22c7a729..09625a222f2 100644 --- a/homeassistant/components/home_plus_control/translations/hu.json +++ b/homeassistant/components/home_plus_control/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse 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.", diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 2314d2b0c1b..8c31859b8e0 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -61,7 +61,7 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no async def async_handle_turn_service(service): """Handle calls to homeassistant.turn_on/off.""" - referenced = await async_extract_referenced_entity_ids(hass, service) + referenced = async_extract_referenced_entity_ids(hass, service) all_referenced = referenced.referenced | referenced.indirectly_referenced # Generic turn on/off method requires entity id diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 9ae271baa72..cd5da46a03a 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -150,9 +150,7 @@ def entities_in_scene(hass: HomeAssistant, entity_id: str) -> list[str]: platform = hass.data[DATA_PLATFORM] - entity = platform.entities.get(entity_id) - - if entity is None: + if (entity := platform.entities.get(entity_id)) is None: return [] return list(entity.scene_config.states) @@ -233,8 +231,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entities = call.data[CONF_ENTITIES] for entity_id in snapshot: - state = hass.states.get(entity_id) - if state is None: + if (state := hass.states.get(entity_id)) is None: _LOGGER.warning( "Entity %s does not exist and therefore cannot be snapshotted", entity_id, @@ -248,8 +245,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= scene_config = SceneConfig(None, call.data[CONF_SCENE_ID], None, entities) entity_id = f"{SCENE_DOMAIN}.{scene_config.name}" - old = platform.entities.get(entity_id) - if old is not None: + if (old := platform.entities.get(entity_id)) is not None: if not old.from_service: _LOGGER.warning("The scene %s already exists", entity_id) return @@ -263,10 +259,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _process_scenes_config(hass, async_add_entities, config): """Process multiple scenes and add them.""" - scene_config = config[STATES] - # Check empty list - if not scene_config: + if not (scene_config := config[STATES]): return async_add_entities( @@ -311,8 +305,7 @@ class HomeAssistantScene(Scene): def extra_state_attributes(self): """Return the scene state attributes.""" attributes = {ATTR_ENTITY_ID: list(self.scene_config.states)} - unique_id = self.unique_id - if unique_id is not None: + if (unique_id := self.unique_id) is not None: attributes[CONF_ID] = unique_id return attributes diff --git a/homeassistant/components/homeassistant/translations/bg.json b/homeassistant/components/homeassistant/translations/bg.json new file mode 100644 index 00000000000..dab7fd6426a --- /dev/null +++ b/homeassistant/components/homeassistant/translations/bg.json @@ -0,0 +1,15 @@ +{ + "system_health": { + "info": { + "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043d\u0430 CPU", + "docker": "Docker", + "hassio": "Supervisor", + "installation_type": "\u0422\u0438\u043f \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f", + "os_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u0442\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430", + "python_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Python", + "timezone": "\u0427\u0430\u0441\u043e\u0432\u0430 \u0437\u043e\u043d\u0430", + "user": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b", + "version": "\u0412\u0435\u0440\u0441\u0438\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 6ca1998a5c3..90780489d7b 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -79,13 +79,11 @@ async def async_attach_trigger(hass, config, action, automation_info): # Check state of entity. If valid, set up a listener. if new_state.domain == "input_datetime": - has_date = new_state.attributes["has_date"] - if has_date: + if has_date := new_state.attributes["has_date"]: year = new_state.attributes["year"] month = new_state.attributes["month"] day = new_state.attributes["day"] - has_time = new_state.attributes["has_time"] - if has_time: + if has_time := new_state.attributes["has_time"]: hour = new_state.attributes["hour"] minute = new_state.attributes["minute"] second = new_state.attributes["second"] diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 8fc68ca641c..9d8c3d04302 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -69,7 +69,6 @@ from .const import ( BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CONF_ADVERTISE_IP, - CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_ENTRY_INDEX, CONF_EXCLUDE_ACCESSORY_MODE, @@ -80,14 +79,10 @@ from .const import ( CONF_LINKED_DOORBELL_SENSOR, CONF_LINKED_HUMIDITY_SENSOR, CONF_LINKED_MOTION_SENSOR, - CONF_SAFE_MODE, - CONF_ZEROCONF_DEFAULT_INTERFACE, CONFIG_OPTIONS, - DEFAULT_AUTO_START, DEFAULT_EXCLUDE_ACCESSORY_MODE, DEFAULT_HOMEKIT_MODE, DEFAULT_PORT, - DEFAULT_SAFE_MODE, DOMAIN, HOMEKIT, HOMEKIT_MODE_ACCESSORY, @@ -141,9 +136,6 @@ def _has_all_unique_names_and_ports(bridges): BRIDGE_SCHEMA = vol.All( - cv.deprecated(CONF_ZEROCONF_DEFAULT_INTERFACE), - cv.deprecated(CONF_SAFE_MODE), - cv.deprecated(CONF_AUTO_START), vol.Schema( { vol.Optional(CONF_HOMEKIT_MODE, default=DEFAULT_HOMEKIT_MODE): vol.In( @@ -155,11 +147,8 @@ BRIDGE_SCHEMA = vol.All( vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), vol.Optional(CONF_ADVERTISE_IP): vol.All(ipaddress.ip_address, cv.string), - vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, - vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, vol.Optional(CONF_FILTER, default={}): BASE_FILTER_SCHEMA, vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, - vol.Optional(CONF_ZEROCONF_DEFAULT_INTERFACE): cv.boolean, vol.Optional(CONF_DEVICES): cv.ensure_list, }, extra=vol.ALLOW_EXTRA, @@ -279,7 +268,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) homekit_mode = options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy() - auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START) entity_filter = FILTER_SCHEMA(options.get(CONF_FILTER, {})) devices = options.get(CONF_DEVICES, []) @@ -307,7 +295,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if hass.state == CoreState.running: await homekit.async_start() - elif auto_start: + else: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, homekit.async_start) return True @@ -396,11 +384,10 @@ def _async_register_events_and_services(hass: HomeAssistant): async def async_handle_homekit_unpair(service): """Handle unpair HomeKit service call.""" - referenced = await async_extract_referenced_entity_ids(hass, service) + referenced = async_extract_referenced_entity_ids(hass, service) dev_reg = device_registry.async_get(hass) for device_id in referenced.referenced_devices: - dev_reg_ent = dev_reg.async_get(device_id) - if not dev_reg_ent: + if not (dev_reg_ent := dev_reg.async_get(device_id)): raise HomeAssistantError(f"No device found for device id: {device_id}") macs = [ cval @@ -697,8 +684,7 @@ class HomeKit: if not self._filter(entity_id): continue - ent_reg_ent = ent_reg.async_get(entity_id) - if ent_reg_ent: + if ent_reg_ent := ent_reg.async_get(entity_id): await self._async_set_device_info_attributes( ent_reg_ent, dev_reg, entity_id ) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 3b7f2c0f9eb..30ee2e72589 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -413,8 +413,7 @@ class HomeAccessory(Accessory): @ha_callback def async_update_linked_battery_callback(self, event): """Handle linked battery sensor state change listener callback.""" - new_state = event.data.get("new_state") - if new_state is None: + if (new_state := event.data.get("new_state")) is None: return if self.linked_battery_charging_sensor: battery_charging_state = None @@ -425,8 +424,7 @@ class HomeAccessory(Accessory): @ha_callback def async_update_linked_battery_charging_callback(self, event): """Handle linked battery charging sensor state change listener callback.""" - new_state = event.data.get("new_state") - if new_state is None: + if (new_state := event.data.get("new_state")) is None: return self.async_update_battery(None, new_state.state == STATE_ON) @@ -524,8 +522,7 @@ class HomeBridge(Bridge): async def async_get_snapshot(self, info): """Get snapshot from accessory if supported.""" - acc = self.accessories.get(info["aid"]) - if acc is None: + if (acc := self.accessories.get(info["aid"])) is None: raise ValueError("Requested snapshot for missing accessory") if not hasattr(acc, "async_get_snapshot"): raise ValueError( diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index ddf3c7c564e..0f5e29426a8 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -92,8 +92,7 @@ class AccessoryAidStorage: def get_or_allocate_aid_for_entity_id(self, entity_id: str): """Generate a stable aid for an entity id.""" - entity = self._entity_registry.async_get(entity_id) - if not entity: + if not (entity := self._entity_registry.async_get(entity_id)): return self.get_or_allocate_aid(None, entity_id) sys_unique_id = get_system_unique_id(entity) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 79fc43fde3a..a79db949ab0 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -36,14 +36,12 @@ from homeassistant.helpers.entityfilter import ( from homeassistant.loader import async_get_integration from .const import ( - CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_EXCLUDE_ACCESSORY_MODE, CONF_FILTER, CONF_HOMEKIT_MODE, CONF_SUPPORT_AUDIO, CONF_VIDEO_CODEC, - DEFAULT_AUTO_START, DEFAULT_CONFIG_FLOW_PORT, DEFAULT_HOMEKIT_MODE, DOMAIN, @@ -311,14 +309,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_advanced(self, user_input=None): """Choose advanced options.""" - if not self.show_advanced_options or user_input is not None: + if ( + not self.show_advanced_options + or user_input is not None + or self.hk_options[CONF_HOMEKIT_MODE] != HOMEKIT_MODE_BRIDGE + ): if user_input: self.hk_options.update(user_input) - self.hk_options[CONF_AUTO_START] = self.hk_options.get( - CONF_AUTO_START, DEFAULT_AUTO_START - ) - for key in (CONF_DOMAINS, CONF_ENTITIES): if key in self.hk_options: del self.hk_options[key] @@ -331,23 +329,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry(title="", data=self.hk_options) - data_schema = { - vol.Optional( - CONF_AUTO_START, - default=self.hk_options.get(CONF_AUTO_START, DEFAULT_AUTO_START), - ): bool - } - - if self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE: - all_supported_devices = await _async_get_supported_devices(self.hass) - devices = self.hk_options.get(CONF_DEVICES, []) - data_schema[vol.Optional(CONF_DEVICES, default=devices)] = cv.multi_select( - all_supported_devices - ) - + all_supported_devices = await _async_get_supported_devices(self.hass) return self.async_show_form( step_id="advanced", - data_schema=vol.Schema(data_schema), + data_schema=vol.Schema( + { + vol.Optional( + CONF_DEVICES, default=self.hk_options.get(CONF_DEVICES, []) + ): cv.multi_select(all_supported_devices) + } + ), ) async def async_step_cameras(self, user_input=None): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 4638c9f3b62..c77efb705da 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -34,7 +34,6 @@ CONF_ADVERTISE_IP = "advertise_ip" CONF_AUDIO_CODEC = "audio_codec" CONF_AUDIO_MAP = "audio_map" CONF_AUDIO_PACKET_SIZE = "audio_packet_size" -CONF_AUTO_START = "auto_start" CONF_ENTITY_CONFIG = "entity_config" CONF_FEATURE = "feature" CONF_FEATURE_LIST = "feature_list" @@ -50,8 +49,6 @@ CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold" CONF_MAX_FPS = "max_fps" CONF_MAX_HEIGHT = "max_height" CONF_MAX_WIDTH = "max_width" -CONF_SAFE_MODE = "safe_mode" -CONF_ZEROCONF_DEFAULT_INTERFACE = "zeroconf_default_interface" CONF_STREAM_ADDRESS = "stream_address" CONF_STREAM_SOURCE = "stream_source" CONF_SUPPORT_AUDIO = "support_audio" @@ -65,7 +62,6 @@ DEFAULT_SUPPORT_AUDIO = False DEFAULT_AUDIO_CODEC = AUDIO_CODEC_OPUS DEFAULT_AUDIO_MAP = "0:a:0" DEFAULT_AUDIO_PACKET_SIZE = 188 -DEFAULT_AUTO_START = True DEFAULT_EXCLUDE_ACCESSORY_MODE = False DEFAULT_LOW_BATTERY_THRESHOLD = 20 DEFAULT_MAX_FPS = 30 @@ -73,7 +69,6 @@ DEFAULT_MAX_HEIGHT = 1080 DEFAULT_MAX_WIDTH = 1920 DEFAULT_PORT = 21063 DEFAULT_CONFIG_FLOW_PORT = 21064 -DEFAULT_SAFE_MODE = False DEFAULT_VIDEO_CODEC = VIDEO_CODEC_LIBX264 DEFAULT_VIDEO_MAP = "0:v:0" DEFAULT_VIDEO_PACKET_SIZE = 1316 @@ -293,8 +288,6 @@ HK_NOT_CHARGABLE = 2 # ### Config Options ### CONFIG_OPTIONS = [ CONF_FILTER, - CONF_AUTO_START, - CONF_SAFE_MODE, CONF_ENTITY_CONFIG, CONF_HOMEKIT_MODE, CONF_DEVICES, diff --git a/homeassistant/components/homekit/translations/bg.json b/homeassistant/components/homekit/translations/bg.json new file mode 100644 index 00000000000..4e5677f124a --- /dev/null +++ b/homeassistant/components/homekit/translations/bg.json @@ -0,0 +1,16 @@ +{ + "options": { + "step": { + "include_exclude": { + "data": { + "mode": "\u0420\u0435\u0436\u0438\u043c" + } + }, + "init": { + "data": { + "mode": "\u0420\u0435\u0436\u0438\u043c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index 2ab21f66db5..3f65f8e5e7f 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -24,7 +24,7 @@ "auto_start": "Autostart (deactiveer als je de homekit.start service handmatig aanroept)", "devices": "Apparaten (triggers)" }, - "description": "Deze instellingen hoeven alleen te worden aangepast als HomeKit niet functioneert.", + "description": "Voor elk geselecteerd apparaat worden programmeerbare schakelaars gemaakt. Wanneer een apparaattrigger wordt geactiveerd, kan HomeKit worden geconfigureerd om een automatisering of sc\u00e8ne uit te voeren.", "title": "Geavanceerde configuratie" }, "cameras": { diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index 300c730e66c..4b25a482cfd 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -29,6 +29,7 @@ }, "cameras": { "data": { + "camera_audio": "Kamery obs\u0142uguj\u0105ce d\u017awi\u0119k", "camera_copy": "Kamery obs\u0142uguj\u0105ce kodek H.264" }, "description": "Sprawd\u017a, czy wszystkie kamery obs\u0142uguj\u0105 kodek H.264. Je\u015bli kamera nie wysy\u0142a strumienia skompresowanego kodekiem H.264, system b\u0119dzie transkodowa\u0142 wideo do H.264 dla HomeKit. Transkodowanie wymaga wydajnego procesora i jest ma\u0142o prawdopodobne, aby dzia\u0142a\u0142o na komputerach jednop\u0142ytkowych.", diff --git a/homeassistant/components/homekit/translations/zh-Hans.json b/homeassistant/components/homekit/translations/zh-Hans.json index 4a1486735ff..73875ea0423 100644 --- a/homeassistant/components/homekit/translations/zh-Hans.json +++ b/homeassistant/components/homekit/translations/zh-Hans.json @@ -13,7 +13,7 @@ "include_domains": "\u8981\u5305\u542b\u7684\u57df" }, "description": "HomeKit \u96c6\u6210\u53ef\u4ee5\u8ba9\u60a8\u901a\u8fc7 HomeKit \u8bbf\u95ee Home Assistant \u4e2d\u7684\u5b9e\u4f53\u3002\u5728\u6865\u63a5\u6a21\u5f0f\u4e2d\uff0c\u6bcf\u4e2a\u6865\u63a5\u5668\u5b9e\u4f8b\u6700\u591a\u53ef\u6a21\u62df 150 \u4e2a\u914d\u4ef6\uff0c\u5305\u62ec\u6865\u63a5\u5668\u672c\u8eab\u3002\u5982\u679c\u60a8\u5e0c\u671b\u6865\u63a5\u7684\u914d\u4ef6\u591a\u4e8e\u6b64\u6570\u91cf\uff0c\u5efa\u8bae\u4e3a\u4e0d\u540c\u7684\u57df\u4f7f\u7528\u591a\u4e2a HomeKit \u6865\u63a5\u5668\u3002\u8be6\u7ec6\u7684\u5b9e\u4f53\u914d\u7f6e\u4ec5\u53ef\u7528\u4e8e\u4e3b\u6865\u63a5\u5668\uff0c\u4e14\u987b\u901a\u8fc7 YAML \u914d\u7f6e\u3002", - "title": "\u6fc0\u6d3b HomeKit" + "title": "\u9009\u62e9\u8981\u5305\u542b\u7684\u57df" } } }, @@ -21,18 +21,19 @@ "step": { "advanced": { "data": { - "auto_start": "[%key_id:43661779%]", + "auto_start": "\u81ea\u52a8\u542f\u52a8\uff08\u5982\u679c\u60a8\u624b\u52a8\u8c03\u7528 homekit.start \u670d\u52a1\uff0c\u8bf7\u7981\u7528\u6b64\u9879\uff09", "devices": "\u8bbe\u5907 (\u89e6\u53d1\u5668)" }, - "description": "\u8fd9\u4e9b\u8bbe\u7f6e\u53ea\u6709\u5f53 HomeKit \u529f\u80fd\u4e0d\u6b63\u5e38\u65f6\u624d\u9700\u8981\u8c03\u6574\u3002", + "description": "\u5c06\u4e3a\u6bcf\u4e2a\u9009\u62e9\u7684\u8bbe\u5907\u521b\u5efa\u4e00\u4e2a\u53ef\u7f16\u7a0b\u5f00\u5173\u914d\u4ef6\u3002\u53ef\u4ee5\u5728 HomeKit \u4e2d\u914d\u7f6e\u8fd9\u4e9b\u914d\u4ef6\uff0c\u5f53\u8bbe\u5907\u89e6\u53d1\u65f6\uff0c\u6267\u884c\u6307\u5b9a\u7684\u81ea\u52a8\u5316\u6216\u573a\u666f\u3002", "title": "\u9ad8\u7ea7\u914d\u7f6e" }, "cameras": { "data": { + "camera_audio": "\u652f\u6301\u97f3\u9891\u7684\u6444\u50cf\u673a", "camera_copy": "\u652f\u6301\u539f\u751f H.264 \u63a8\u6d41\u7684\u6444\u50cf\u673a" }, "description": "\u67e5\u627e\u6240\u6709\u652f\u6301\u539f\u751f H.264 \u63a8\u6d41\u7684\u6444\u50cf\u673a\u3002\u5982\u679c\u6444\u50cf\u673a\u8f93\u51fa\u7684\u4e0d\u662f H.264 \u6d41\uff0c\u7cfb\u7edf\u4f1a\u5c06\u89c6\u9891\u8f6c\u7801\u4e3a H.264 \u4ee5\u4f9b HomeKit \u4f7f\u7528\u3002\u8f6c\u7801\u9700\u8981\u9ad8\u6027\u80fd\u7684 CPU\uff0c\u56e0\u6b64\u5728\u5f00\u53d1\u677f\u8ba1\u7b97\u673a\u4e0a\u5f88\u96be\u5b8c\u6210\u3002", - "title": "\u8bf7\u9009\u62e9\u6444\u50cf\u673a\u7684\u89c6\u9891\u7f16\u7801\u3002" + "title": "\u6444\u50cf\u673a\u914d\u7f6e" }, "include_exclude": { "data": { @@ -40,7 +41,7 @@ "mode": "\u6a21\u5f0f" }, "description": "\u9009\u62e9\u8981\u5f00\u653e\u7684\u5b9e\u4f53\u3002\u5728\u9644\u4ef6\u6a21\u5f0f\u4e2d\uff0c\u53ea\u80fd\u5f00\u653e\u4e00\u4e2a\u5b9e\u4f53\u3002\u5728\u6865\u63a5\u5305\u542b\u6a21\u5f0f\u4e2d\uff0c\u5982\u679c\u4e0d\u9009\u62e9\u5305\u542b\u7684\u5b9e\u4f53\uff0c\u57df\u4e2d\u6240\u6709\u5b9e\u4f53\u90fd\u4f1a\u5f00\u653e\u3002\u5728\u6865\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\uff0c\u5982\u679c\u4e0d\u9009\u62e9\u6392\u9664\u7684\u5b9e\u4f53\uff0c\u57df\u4e2d\u6240\u6709\u5b9e\u4f53\u4e5f\u90fd\u4f1a\u5f00\u653e\u3002", - "title": "\u9009\u62e9\u8981\u5f00\u653e\u7684\u5b9e\u4f53" + "title": "\u9009\u62e9\u8981\u5305\u542b\u7684\u5b9e\u4f53" }, "init": { "data": { @@ -48,7 +49,7 @@ "mode": "\u6a21\u5f0f" }, "description": "HomeKit \u53ef\u4ee5\u88ab\u914d\u7f6e\u4e3a\u5bf9\u5916\u5c55\u793a\u4e00\u4e2a\u6865\u63a5\u5668\u6216\u5355\u4e2a\u914d\u4ef6\u3002\u5728\u914d\u4ef6\u6a21\u5f0f\u4e2d\uff0c\u53ea\u80fd\u4f7f\u7528\u4e00\u4e2a\u5b9e\u4f53\u3002\u8bbe\u5907\u7c7b\u578b\u4e3a\u201c\u7535\u89c6\u201d\u7684\u5a92\u4f53\u64ad\u653e\u5668\u5fc5\u987b\u4f7f\u7528\u914d\u4ef6\u6a21\u5f0f\u624d\u80fd\u6b63\u5e38\u5de5\u4f5c\u3002\u201c\u8981\u5305\u542b\u7684\u57df\u201d\u4e2d\u7684\u5b9e\u4f53\u5c06\u5411 HomeKit \u5f00\u653e\u3002\u5728\u4e0b\u4e00\u9875\u53ef\u4ee5\u9009\u62e9\u8981\u5305\u542b\u6216\u6392\u9664\u5176\u4e2d\u7684\u54ea\u4e9b\u5b9e\u4f53\u3002", - "title": "\u9009\u62e9\u8981\u5f00\u653e\u7684\u57df\u3002" + "title": "\u9009\u62e9\u8981\u5305\u542b\u7684\u57df\u3002" }, "yaml": { "description": "\u8be5\u6761\u76ee\u662f\u901a\u8fc7 YAML \u63a7\u5236\u7684", diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 2cdcd600932..6cf8735b075 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -314,8 +314,7 @@ class Camera(HomeAccessory, PyhapCamera): async def _async_get_stream_source(self): """Find the camera stream source url.""" - stream_source = self.config.get(CONF_STREAM_SOURCE) - if stream_source: + if stream_source := self.config.get(CONF_STREAM_SOURCE): return stream_source try: stream_source = await self.hass.components.camera.async_get_stream_source( @@ -447,8 +446,7 @@ class Camera(HomeAccessory, PyhapCamera): async def stop_stream(self, session_info): """Stop the stream for the given ``session_id``.""" session_id = session_info["id"] - stream = session_info.get("stream") - if not stream: + if not (stream := session_info.get("stream")): _LOGGER.debug("No stream for session ID %s", session_id) return diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 7be1b98dcdb..61c043ebcaa 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -303,8 +303,7 @@ class TelevisionMediaPlayer(RemoteInputSelectAccessory): def set_remote_key(self, value): """Send remote key value if call came from HomeKit.""" _LOGGER.debug("%s: Set remote key to %s", self.entity_id, value) - key_name = REMOTE_KEYS.get(value) - if key_name is None: + if (key_name := REMOTE_KEYS.get(value)) is None: _LOGGER.warning("%s: Unhandled key press for %s", self.entity_id, value) return diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py index 53659adef77..41a76ca7fed 100644 --- a/homeassistant/components/homekit/type_remotes.py +++ b/homeassistant/components/homekit/type_remotes.py @@ -207,8 +207,7 @@ class ActivityRemote(RemoteInputSelectAccessory): def set_remote_key(self, value): """Send remote key value if call came from HomeKit.""" _LOGGER.debug("%s: Set remote key to %s", self.entity_id, value) - key_name = REMOTE_KEYS.get(value) - if key_name is None: + if (key_name := REMOTE_KEYS.get(value)) is None: _LOGGER.warning("%s: Unhandled key press for %s", self.entity_id, value) return self.hass.bus.async_fire( diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index bcef7564fa3..c309e42a0f0 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -1,5 +1,8 @@ """Class to hold all sensor accessories.""" +from __future__ import annotations + import logging +from typing import Callable, NamedTuple from pyhap.const import CATEGORY_SENSOR @@ -60,18 +63,31 @@ from .util import convert_to_float, density_to_air_quality, temperature_to_homek _LOGGER = logging.getLogger(__name__) -BINARY_SENSOR_SERVICE_MAP = { - DEVICE_CLASS_CO: (SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int), - DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, int), - DEVICE_CLASS_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), - DEVICE_CLASS_GARAGE_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), - DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int), - DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, int), - DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, bool), - DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, int), - DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), - DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED, int), - DEVICE_CLASS_WINDOW: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), + +class SI(NamedTuple): + """Service info.""" + + service: str + char: str + format: Callable[[bool], int | bool] + + +BINARY_SENSOR_SERVICE_MAP: dict[str, SI] = { + DEVICE_CLASS_CO: SI( + SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int + ), + DEVICE_CLASS_CO2: SI(SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, int), + DEVICE_CLASS_DOOR: SI(SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), + DEVICE_CLASS_GARAGE_DOOR: SI(SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), + DEVICE_CLASS_GAS: SI( + SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int + ), + DEVICE_CLASS_MOISTURE: SI(SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, int), + DEVICE_CLASS_MOTION: SI(SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, bool), + DEVICE_CLASS_OCCUPANCY: SI(SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, int), + DEVICE_CLASS_OPENING: SI(SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), + DEVICE_CLASS_SMOKE: SI(SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED, int), + DEVICE_CLASS_WINDOW: SI(SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), } @@ -276,11 +292,11 @@ class BinarySensor(HomeAccessory): else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY] ) - self.format = service_char[2] - service = self.add_preload_service(service_char[0]) + self.format = service_char.format + service = self.add_preload_service(service_char.service) initial_value = False if self.format is bool else 0 self.char_detected = service.configure_char( - service_char[1], value=initial_value + service_char.char, value=initial_value ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 4e76b0369fe..9b9ff1f4df2 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,5 +1,8 @@ """Class to hold all switch accessories.""" +from __future__ import annotations + import logging +from typing import NamedTuple from pyhap.const import ( CATEGORY_FAUCET, @@ -50,11 +53,19 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -VALVE_TYPE = { - TYPE_FAUCET: (CATEGORY_FAUCET, 3), - TYPE_SHOWER: (CATEGORY_SHOWER_HEAD, 2), - TYPE_SPRINKLER: (CATEGORY_SPRINKLER, 1), - TYPE_VALVE: (CATEGORY_FAUCET, 0), + +class ValveInfo(NamedTuple): + """Category and type information for valve.""" + + category: int + valve_type: int + + +VALVE_TYPE: dict[str, ValveInfo] = { + TYPE_FAUCET: ValveInfo(CATEGORY_FAUCET, 3), + TYPE_SHOWER: ValveInfo(CATEGORY_SHOWER_HEAD, 2), + TYPE_SPRINKLER: ValveInfo(CATEGORY_SPRINKLER, 1), + TYPE_VALVE: ValveInfo(CATEGORY_FAUCET, 0), } @@ -199,7 +210,7 @@ class Valve(HomeAccessory): super().__init__(*args) state = self.hass.states.get(self.entity_id) valve_type = self.config[CONF_TYPE] - self.category = VALVE_TYPE[valve_type][0] + self.category = VALVE_TYPE[valve_type].category serv_valve = self.add_preload_service(SERV_VALVE) self.char_active = serv_valve.configure_char( @@ -207,7 +218,7 @@ class Valve(HomeAccessory): ) self.char_in_use = serv_valve.configure_char(CHAR_IN_USE, value=False) self.char_valve_type = serv_valve.configure_char( - CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type][1] + CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type].valve_type ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 95c5f87b6c2..804f0b86167 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -462,8 +462,7 @@ class Thermostat(HomeAccessory): ) # Set current operation mode for supported thermostats - hvac_action = new_state.attributes.get(ATTR_HVAC_ACTION) - if hvac_action: + if hvac_action := new_state.attributes.get(ATTR_HVAC_ACTION): homekit_hvac_action = HC_HASS_TO_HOMEKIT_ACTION[hvac_action] self.char_current_heat_cool.set_value(homekit_hvac_action) @@ -575,8 +574,7 @@ class WaterHeater(HomeAccessory): def set_heat_cool(self, value): """Change operation mode to value if call came from HomeKit.""" _LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) - hass_value = HC_HOMEKIT_TO_HASS[value] - if hass_value != HVAC_MODE_HEAT: + if HC_HOMEKIT_TO_HASS[value] != HVAC_MODE_HEAT: self.char_target_heat_cool.set_value(1) # Heat def set_target_temperature(self, value): @@ -609,21 +607,18 @@ class WaterHeater(HomeAccessory): self.char_display_units.set_value(unit) # Update target operation mode - operation_mode = new_state.state - if operation_mode: + if new_state.state: self.char_target_heat_cool.set_value(1) # Heat def _get_temperature_range_from_state(state, unit, default_min, default_max): """Calculate the temperature range from a state.""" - min_temp = state.attributes.get(ATTR_MIN_TEMP) - if min_temp: + if min_temp := state.attributes.get(ATTR_MIN_TEMP): min_temp = round(temperature_to_homekit(min_temp, unit) * 2) / 2 else: min_temp = default_min - max_temp = state.attributes.get(ATTR_MAX_TEMP) - if max_temp: + if max_temp := state.attributes.get(ATTR_MAX_TEMP): max_temp = round(temperature_to_homekit(max_temp, unit) * 2) / 2 else: max_temp = default_max diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index f7c98c66708..f91355906dc 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -14,13 +14,21 @@ from aiohomekit.model.characteristics import ( from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components import zeroconf -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ATTR_VIA_DEVICE, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .config_flow import normalize_hkid -from .connection import HKDevice -from .const import CONTROLLER, DOMAIN, ENTITY_MAP, KNOWN_DEVICES, TRIGGERS +from .connection import HKDevice, valid_serial_number +from .const import ( + CONTROLLER, + DOMAIN, + ENTITY_MAP, + IDENTIFIER_ACCESSORY_ID, + IDENTIFIER_SERIAL_NUMBER, + KNOWN_DEVICES, + TRIGGERS, +) from .storage import EntityMapStorage @@ -131,8 +139,12 @@ class HomeKitEntity(Entity): @property def unique_id(self) -> str: """Return the ID of this device.""" - serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) - return f"homekit-{serial}-{self._iid}" + info = self.accessory_info + serial = info.value(CharacteristicsTypes.SERIAL_NUMBER) + if valid_serial_number(serial): + return f"homekit-{serial}-{self._iid}" + # Some accessories do not have a serial number + return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}" @property def name(self) -> str: @@ -145,24 +157,37 @@ class HomeKitEntity(Entity): return self._accessory.available and self.service.available @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" info = self.accessory_info accessory_serial = info.value(CharacteristicsTypes.SERIAL_NUMBER) + if valid_serial_number(accessory_serial): + # Some accessories do not have a serial number + identifier = (DOMAIN, IDENTIFIER_SERIAL_NUMBER, accessory_serial) + else: + identifier = ( + DOMAIN, + IDENTIFIER_ACCESSORY_ID, + f"{self._accessory.unique_id}_{self._aid}", + ) - device_info = { - "identifiers": {(DOMAIN, "serial-number", accessory_serial)}, - "name": info.value(CharacteristicsTypes.NAME), - "manufacturer": info.value(CharacteristicsTypes.MANUFACTURER, ""), - "model": info.value(CharacteristicsTypes.MODEL, ""), - "sw_version": info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""), - } + device_info = DeviceInfo( + identifiers={identifier}, + manufacturer=info.value(CharacteristicsTypes.MANUFACTURER, ""), + model=info.value(CharacteristicsTypes.MODEL, ""), + name=info.value(CharacteristicsTypes.NAME), + sw_version=info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""), + ) # Some devices only have a single accessory - we don't add a # via_device otherwise it would be self referential. bridge_serial = self._accessory.connection_info["serial-number"] if accessory_serial != bridge_serial: - device_info["via_device"] = (DOMAIN, "serial-number", bridge_serial) + device_info[ATTR_VIA_DEVICE] = ( + DOMAIN, + IDENTIFIER_SERIAL_NUMBER, + bridge_serial, + ) return device_info diff --git a/homeassistant/components/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py index b4ca2f4918a..df5a89f179e 100644 --- a/homeassistant/components/homekit_controller/air_quality.py +++ b/homeassistant/components/homekit_controller/air_quality.py @@ -91,8 +91,7 @@ class HomeAirQualitySensor(HomeKitEntity, AirQualityEntity): """Return the device state attributes.""" data = {"air_quality_text": self.air_quality_text} - voc = self.volatile_organic_compounds - if voc: + if voc := self.volatile_organic_compounds: data["volatile_organic_compounds"] = voc return data diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 64257c47f47..ad079f8322d 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -123,8 +123,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(service): - entity_class = ENTITY_TYPES.get(service.short_type) - if not entity_class: + if not (entity_class := ENTITY_TYPES.get(service.short_type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index ec0f383356e..cd9b1fe004c 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -13,11 +13,7 @@ from aiohomekit.model.characteristics import ( from aiohomekit.model.services import ServicesTypes from aiohomekit.utils import clamp_enum_to_char -from homeassistant.components.climate import ( - DEFAULT_MAX_HUMIDITY, - DEFAULT_MIN_HUMIDITY, - ClimateEntity, -) +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, @@ -93,8 +89,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(service): - entity_class = ENTITY_TYPES.get(service.short_type) - if not entity_class: + if not (entity_class := ENTITY_TYPES.get(service.short_type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) @@ -488,14 +483,22 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): @property def min_humidity(self): """Return the minimum humidity.""" - char = self.service[CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET] - return char.minValue or DEFAULT_MIN_HUMIDITY + min_humidity = self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET + ].minValue + if min_humidity is not None: + return min_humidity + return super().min_humidity @property def max_humidity(self): """Return the maximum humidity.""" - char = self.service[CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET] - return char.maxValue or DEFAULT_MAX_HUMIDITY + max_humidity = self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET + ].maxValue + if max_humidity is not None: + return max_humidity + return super().max_humidity @property def hvac_action(self): diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 16866bedcfe..8523fec7b8f 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -12,8 +12,10 @@ from aiohomekit.model import Accessories from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes +from homeassistant.const import ATTR_VIA_DEVICE from homeassistant.core import callback from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from .const import ( @@ -22,6 +24,8 @@ from .const import ( DOMAIN, ENTITY_MAP, HOMEKIT_ACCESSORY_DISPATCH, + IDENTIFIER_ACCESSORY_ID, + IDENTIFIER_SERIAL_NUMBER, ) from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry @@ -32,6 +36,16 @@ MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 _LOGGER = logging.getLogger(__name__) +def valid_serial_number(serial): + """Return if the serial number appears to be valid.""" + if not serial: + return False + try: + return float("".join(serial.rsplit(".", 1))) > 1 + except ValueError: + return True + + def get_accessory_information(accessory): """Obtain the accessory information service of a HomeKit device.""" result = {} @@ -205,31 +219,40 @@ class HKDevice: service_type=ServicesTypes.ACCESSORY_INFORMATION, ) - device_info = { - "identifiers": { + serial_number = info.value(CharacteristicsTypes.SERIAL_NUMBER) + + if valid_serial_number(serial_number): + identifiers = {(DOMAIN, IDENTIFIER_SERIAL_NUMBER, serial_number)} + else: + # Some accessories do not have a serial number + identifiers = { ( DOMAIN, - "serial-number", - info.value(CharacteristicsTypes.SERIAL_NUMBER), + IDENTIFIER_ACCESSORY_ID, + f"{self.unique_id}_{accessory.aid}", ) - }, - "name": info.value(CharacteristicsTypes.NAME), - "manufacturer": info.value(CharacteristicsTypes.MANUFACTURER, ""), - "model": info.value(CharacteristicsTypes.MODEL, ""), - "sw_version": info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""), - } + } if accessory.aid == 1: # Accessory 1 is the root device (sometimes the only device, sometimes a bridge) # Link the root device to the pairing id for the connection. - device_info["identifiers"].add((DOMAIN, "accessory-id", self.unique_id)) - else: + identifiers.add((DOMAIN, IDENTIFIER_ACCESSORY_ID, self.unique_id)) + + device_info = DeviceInfo( + identifiers=identifiers, + name=info.value(CharacteristicsTypes.NAME), + manufacturer=info.value(CharacteristicsTypes.MANUFACTURER, ""), + model=info.value(CharacteristicsTypes.MODEL, ""), + sw_version=info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""), + ) + + if accessory.aid != 1: # Every pairing has an accessory 1 # It *doesn't* have a via_device, as it is the device we are connecting to # Every other accessory should use it as its via device. - device_info["via_device"] = ( + device_info[ATTR_VIA_DEVICE] = ( DOMAIN, - "serial-number", + IDENTIFIER_SERIAL_NUMBER, self.connection_info["serial-number"], ) diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index fa28bab7606..3c9372f96db 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -11,6 +11,10 @@ TRIGGERS = f"{DOMAIN}-triggers" HOMEKIT_DIR = ".homekit" PAIRING_FILE = "pairing.json" +IDENTIFIER_SERIAL_NUMBER = "serial-number" +IDENTIFIER_ACCESSORY_ID = "accessory-id" + + # Mapping from Homekit type to component. HOMEKIT_ACCESSORY_DISPATCH = { "lightbulb": "light", diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index dd25e32b3c4..ee736bd2c48 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -41,8 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(service): - entity_class = ENTITY_TYPES.get(service.short_type) - if not entity_class: + if not (entity_class := ENTITY_TYPES.get(service.short_type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) @@ -73,7 +72,7 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverEntity): return SUPPORT_OPEN | SUPPORT_CLOSE @property - def state(self): + def _state(self): """Return the current state of the garage door.""" value = self.service.value(CharacteristicsTypes.DOOR_STATE_CURRENT) return CURRENT_GARAGE_STATE_MAP[value] @@ -81,17 +80,17 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverEntity): @property def is_closed(self): """Return true if cover is closed, else False.""" - return self.state == STATE_CLOSED + return self._state == STATE_CLOSED @property def is_closing(self): """Return if the cover is closing or not.""" - return self.state == STATE_CLOSING + return self._state == STATE_CLOSING @property def is_opening(self): """Return if the cover is opening or not.""" - return self.state == STATE_OPENING + return self._state == STATE_OPENING async def async_open_cover(self, **kwargs): """Send open command.""" diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 89f24a66a94..0c0c9ccda9c 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -154,8 +154,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(service): - entity_class = ENTITY_TYPES.get(service.short_type) - if not entity_class: + if not (entity_class := ENTITY_TYPES.get(service.short_type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 79130bfcef7..beb0ffa7fa5 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -4,22 +4,26 @@ Support for Homekit number ranges. These are mostly used where a HomeKit accessory exposes additional non-standard characteristics that don't map to a Home Assistant feature. """ +from __future__ import annotations + from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes -from homeassistant.components.number import NumberEntity +from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.core import callback from . import KNOWN_DEVICES, CharacteristicEntity -NUMBER_ENTITIES = { - CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: { - "name": "Spray Quantity", - "icon": "mdi:water", - }, - CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: { - "name": "Elevation", - "icon": "mdi:elevation-rise", - }, +NUMBER_ENTITIES: dict[str, NumberEntityDescription] = { + CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: NumberEntityDescription( + key=CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL, + name="Spray Quantity", + icon="mdi:water", + ), + CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: NumberEntityDescription( + key=CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION, + name="Elevation", + icon="mdi:elevation-rise", + ), } @@ -30,11 +34,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_characteristic(char: Characteristic): - kwargs = NUMBER_ENTITIES.get(char.type) - if not kwargs: + if not (description := NUMBER_ENTITIES.get(char.type)): return False info = {"aid": char.service.accessory.aid, "iid": char.service.iid} - async_add_entities([HomeKitNumber(conn, info, char, **kwargs)], True) + async_add_entities([HomeKitNumber(conn, info, char, description)], True) return True conn.add_char_factory(async_add_characteristic) @@ -48,32 +51,16 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): conn, info, char, - device_class=None, - icon=None, - name=None, - **kwargs, + description: NumberEntityDescription, ): """Initialise a HomeKit number control.""" - self._device_class = device_class - self._icon = icon - self._name = name - + self.entity_description = description super().__init__(conn, info, char) def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [self._char.type] - @property - def device_class(self): - """Return type of sensor.""" - return self._device_class - - @property - def icon(self): - """Return the sensor icon.""" - return self._icon - @property def min_value(self) -> float: """Return the minimum value.""" diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index cac61d59ac4..15324a2436e 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -1,8 +1,17 @@ """Support for Homekit sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, @@ -29,98 +38,119 @@ from homeassistant.core import callback from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity -HUMIDITY_ICON = "mdi:water-percent" -TEMP_C_ICON = "mdi:thermometer" -BRIGHTNESS_ICON = "mdi:brightness-6" CO2_ICON = "mdi:molecule-co2" -SIMPLE_SENSOR = { - CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: { - "name": "Real Time Energy", - "device_class": DEVICE_CLASS_POWER, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": POWER_WATT, - }, - CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: { - "name": "Real Time Energy", - "device_class": DEVICE_CLASS_POWER, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": POWER_WATT, - }, - CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: { - "name": "Real Time Energy", - "device_class": DEVICE_CLASS_POWER, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": POWER_WATT, - }, - CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: { - "name": "Air Pressure", - "device_class": DEVICE_CLASS_PRESSURE, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": PRESSURE_HPA, - }, - CharacteristicsTypes.TEMPERATURE_CURRENT: { - "name": "Current Temperature", - "device_class": DEVICE_CLASS_TEMPERATURE, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": TEMP_CELSIUS, +@dataclass +class HomeKitSensorEntityDescription(SensorEntityDescription): + """Describes Homekit sensor.""" + + probe: Callable[[Characteristic], bool] | None = None + + +SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { + CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.Vendor.EVE_ENERGY_WATT, + name="Real Time Energy", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), + CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY, + name="Real Time Energy", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), + CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2, + name="Real Time Energy", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), + CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE, + name="Air Pressure", + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PRESSURE_HPA, + ), + CharacteristicsTypes.TEMPERATURE_CURRENT: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.TEMPERATURE_CURRENT, + name="Current Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=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.get_uuid(ServicesTypes.TEMPERATURE_SENSOR), - }, - CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: { - "name": "Current Humidity", - "device_class": DEVICE_CLASS_HUMIDITY, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": PERCENTAGE, + probe=( + lambda char: char.service.type + != ServicesTypes.get_uuid(ServicesTypes.TEMPERATURE_SENSOR) + ), + ), + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, + name="Current Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, # This sensor is only for humidity characteristics that are not part # of a humidity sensor service. - "probe": lambda char: char.service.type - != ServicesTypes.get_uuid(ServicesTypes.HUMIDITY_SENSOR), - }, - CharacteristicsTypes.AIR_QUALITY: { - "name": "Air Quality", - "device_class": DEVICE_CLASS_AQI, - "state_class": STATE_CLASS_MEASUREMENT, - }, - CharacteristicsTypes.DENSITY_PM25: { - "name": "PM2.5 Density", - "device_class": DEVICE_CLASS_PM25, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - CharacteristicsTypes.DENSITY_PM10: { - "name": "PM10 Density", - "device_class": DEVICE_CLASS_PM10, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - CharacteristicsTypes.DENSITY_OZONE: { - "name": "Ozone Density", - "device_class": DEVICE_CLASS_OZONE, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - CharacteristicsTypes.DENSITY_NO2: { - "name": "Nitrogen Dioxide Density", - "device_class": DEVICE_CLASS_NITROGEN_DIOXIDE, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - CharacteristicsTypes.DENSITY_SO2: { - "name": "Sulphur Dioxide Density", - "device_class": DEVICE_CLASS_SULPHUR_DIOXIDE, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - CharacteristicsTypes.DENSITY_VOC: { - "name": "Volatile Organic Compound Density", - "device_class": DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, + probe=( + lambda char: char.service.type + != ServicesTypes.get_uuid(ServicesTypes.HUMIDITY_SENSOR) + ), + ), + CharacteristicsTypes.AIR_QUALITY: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.AIR_QUALITY, + name="Air Quality", + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ), + CharacteristicsTypes.DENSITY_PM25: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.DENSITY_PM25, + name="PM2.5 Density", + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + CharacteristicsTypes.DENSITY_PM10: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.DENSITY_PM10, + name="PM10 Density", + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + CharacteristicsTypes.DENSITY_OZONE: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.DENSITY_OZONE, + name="Ozone Density", + device_class=DEVICE_CLASS_OZONE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + CharacteristicsTypes.DENSITY_NO2: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.DENSITY_NO2, + name="Nitrogen Dioxide Density", + device_class=DEVICE_CLASS_NITROGEN_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + CharacteristicsTypes.DENSITY_SO2: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.DENSITY_SO2, + name="Sulphur Dioxide Density", + device_class=DEVICE_CLASS_SULPHUR_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + CharacteristicsTypes.DENSITY_VOC: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.DENSITY_VOC, + name="Volatile Organic Compound Density", + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), } # For legacy reasons, "built-in" characteristic types are in their short form @@ -148,11 +178,6 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): """Return the name of the device.""" return f"{super().name} Humidity" - @property - def icon(self): - """Return the sensor icon.""" - return HUMIDITY_ICON - @property def native_value(self): """Return the current humidity.""" @@ -174,11 +199,6 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): """Return the name of the device.""" return f"{super().name} Temperature" - @property - def icon(self): - """Return the sensor icon.""" - return TEMP_C_ICON - @property def native_value(self): """Return the current temperature in Celsius.""" @@ -200,11 +220,6 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): """Return the name of the device.""" return f"{super().name} Light Level" - @property - def icon(self): - """Return the sensor icon.""" - return BRIGHTNESS_ICON - @property def native_value(self): """Return the current light level in lux.""" @@ -303,55 +318,27 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): be multiple entities per HomeKit service (this was not previously the case). """ + entity_description: HomeKitSensorEntityDescription + def __init__( self, conn, info, char, - device_class=None, - state_class=None, - unit=None, - icon=None, - name=None, - **kwargs, + description: HomeKitSensorEntityDescription, ): """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 - + self.entity_description = description super().__init__(conn, info, char) def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [self._char.type] - @property - def device_class(self): - """Return type of sensor.""" - return self._device_class - - @property - def state_class(self): - """Return type of state.""" - return self._state_class - - @property - def native_unit_of_measurement(self): - """Return units for the sensor.""" - return self._unit - - @property - def icon(self): - """Return the sensor icon.""" - return self._icon - @property def name(self) -> str: """Return the name of the device if any.""" - return f"{super().name} - {self._name}" + return f"{super().name} - {self.entity_description.name}" @property def native_value(self): @@ -375,8 +362,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(service): - entity_class = ENTITY_TYPES.get(service.short_type) - if not entity_class: + if not (entity_class := ENTITY_TYPES.get(service.short_type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) @@ -386,13 +372,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_characteristic(char: Characteristic): - kwargs = SIMPLE_SENSOR.get(char.type) - if not kwargs: + if not (description := SIMPLE_SENSOR.get(char.type)): return False - if "probe" in kwargs and not kwargs["probe"](char): + if description.probe and not description.probe(char): return False info = {"aid": char.service.accessory.aid, "iid": char.service.iid} - async_add_entities([SimpleSensor(conn, info, char, **kwargs)], True) + async_add_entities([SimpleSensor(conn, info, char, description)], True) return True diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 36ed379bc80..4ae9ed5a5f0 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -110,8 +110,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(service): - entity_class = ENTITY_TYPES.get(service.short_type) - if not entity_class: + if not (entity_class := ENTITY_TYPES.get(service.short_type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) diff --git a/homeassistant/components/homekit_controller/translations/bg.json b/homeassistant/components/homekit_controller/translations/bg.json index 01439889734..4bedc7bcb27 100644 --- a/homeassistant/components/homekit_controller/translations/bg.json +++ b/homeassistant/components/homekit_controller/translations/bg.json @@ -45,7 +45,8 @@ "button6": "\u0411\u0443\u0442\u043e\u043d 6", "button7": "\u0411\u0443\u0442\u043e\u043d 7", "button8": "\u0411\u0443\u0442\u043e\u043d 8", - "button9": "\u0411\u0443\u0442\u043e\u043d 9" + "button9": "\u0411\u0443\u0442\u043e\u043d 9", + "doorbell": "\u0417\u0432\u044a\u043d\u0435\u0446 \u043d\u0430 \u0432\u0440\u0430\u0442\u0430\u0442\u0430" } }, "title": "HomeKit \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/homekit_controller/translations/he.json b/homeassistant/components/homekit_controller/translations/he.json index 1028351a1bc..9593bbd90e4 100644 --- a/homeassistant/components/homekit_controller/translations/he.json +++ b/homeassistant/components/homekit_controller/translations/he.json @@ -3,6 +3,13 @@ "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}" + "flow_title": "{name}", + "step": { + "user": { + "data": { + "device": "\u05d4\u05ea\u05e7\u05df" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index 7703925ae67..6fad9050a20 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "Nem adhat\u00f3 hozz\u00e1 p\u00e1ros\u00edt\u00e1s, mert az eszk\u00f6z m\u00e1r nem tal\u00e1lhat\u00f3.", "already_configured": "A tartoz\u00e9k m\u00e1r konfigur\u00e1lva van ezzel a vez\u00e9rl\u0151vel.", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "already_paired": "Ez a tartoz\u00e9k m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik eszk\u00f6zzel. \u00c1ll\u00edtsa alaphelyzetbe a tartoz\u00e9kot, majd pr\u00f3b\u00e1lkozzon \u00fajra.", "ignored_model": "A HomeKit t\u00e1mogat\u00e1sa e modelln\u00e9l blokkolva van, mivel a szolg\u00e1ltat\u00e1shoz teljes nat\u00edv integr\u00e1ci\u00f3 \u00e9rhet\u0151 el.", "invalid_config_entry": "Ez az eszk\u00f6z k\u00e9szen \u00e1ll a p\u00e1ros\u00edt\u00e1sra, de m\u00e1r van egy \u00fctk\u00f6z\u0151 konfigur\u00e1ci\u00f3s bejegyz\u00e9s Home Assistantban, amelyet el\u0151sz\u00f6r el kell t\u00e1vol\u00edtani.", diff --git a/homeassistant/components/homekit_controller/translations/zh-Hans.json b/homeassistant/components/homekit_controller/translations/zh-Hans.json index 7da392179f6..a5f57e2f576 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hans.json @@ -44,7 +44,7 @@ "data": { "device": "\u8bbe\u5907" }, - "description": "\u9009\u62e9\u60a8\u8981\u914d\u5bf9\u7684\u8bbe\u5907", + "description": "HomeKit \u63a7\u5236\u5668\u4f7f\u7528\u5b89\u5168\u7684\u52a0\u5bc6\u8fde\u63a5\uff0c\u901a\u8fc7\u5c40\u57df\u7f51\u76f4\u63a5\u8fdb\u884c\u901a\u4fe1\uff0c\u65e0\u9700\u5355\u72ec\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud\u3002\u8bf7\u9009\u62e9\u8981\u914d\u5bf9\u7684\u8bbe\u5907\uff1a", "title": "\u4e0e HomeKit \u914d\u4ef6\u914d\u5bf9" } } diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index aa5fb4a8e44..84c722db1df 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -133,8 +133,7 @@ class HMThermostat(HMDevice, ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return None self._hmdevice.writeNodeData(self._state, float(temperature)) diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index 0880d168375..8aaa3ea21ac 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -59,10 +59,12 @@ HM_DEVICE_TYPES = { "IPMultiIO", "IPWSwitch", "IOSwitchWireless", + "IPSwitchRssiDevice", "IPWIODevice", "IPSwitchBattery", "IPMultiIOPCB", "IPGarageSwitch", + "IPWHS2", ], DISCOVER_LIGHTS: [ "Dimmer", @@ -145,9 +147,11 @@ HM_DEVICE_TYPES = { "ShutterContact", "Smoke", "SmokeV2", + "SmokeV2Team", "Motion", "MotionV2", "MotionIP", + "MotionIPContactSabotage", "RemoteMotion", "WeatherSensor", "TiltSensor", @@ -174,6 +178,8 @@ HM_DEVICE_TYPES = { "IPRainSensor", "IPLanRouter", "IPMultiIOPCB", + "IPLockDLD", + "IPWHS2", ], DISCOVER_COVER: [ "Blind", @@ -221,6 +227,10 @@ HM_ATTRIBUTE_SUPPORT = { "OPERATING_VOLTAGE": ["voltage", {}], "WORKING": ["working", {0: "No", 1: "Yes"}], "STATE_UNCERTAIN": ["state_uncertain", {}], + "SENDERID": ["last_senderid", {}], + "SENDERADDRESS": ["last_senderaddress", {}], + "ERROR_ALARM_TEST": ["error_alarm_test", {0: "No", 1: "Yes"}], + "ERROR_SMOKE_CHAMBER": ["error_smoke_chamber", {0: "No", 1: "Yes"}], } HM_PRESS_EVENTS = [ diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index f500ef54b56..896470f5a42 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.74"], + "requirements": ["pyhomematic==0.1.76"], "codeowners": ["@pvizeli", "@danielperna84"], "iot_class": "local_push" } diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 18690ac3553..84bb7b4d5a3 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -42,6 +42,7 @@ HM_STATE_HA_CAST = { 2: "allsens_armed", 3: "alarm_blocked", }, + "IPLockDLD": {0: None, 1: "locked", 2: "unlocked"}, } HM_UNIT_HA_CAST = { diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 212737b7018..d2dee3c3744 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -47,13 +47,13 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "identifiers": {(HMIPC_DOMAIN, f"ACP {self._home.id}")}, - "name": self.name, - "manufacturer": "eQ-3", - "model": CONST_ALARM_CONTROL_PANEL_NAME, - "via_device": (HMIPC_DOMAIN, self._home.id), - } + return DeviceInfo( + identifiers={(HMIPC_DOMAIN, f"ACP {self._home.id}")}, + manufacturer="eQ-3", + model=CONST_ALARM_CONTROL_PANEL_NAME, + name=self.name, + via_device=(HMIPC_DOMAIN, self._home.id), + ) @property def state(self) -> str: diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 80dfa8316d0..b1261258bf4 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -172,12 +172,12 @@ class HomematicipCloudConnectionSensor(HomematicipGenericEntity, BinarySensorEnt def device_info(self) -> DeviceInfo: """Return device specific attributes.""" # Adds a sensor to the existing HAP device - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers of Homematic IP device (HMIPC_DOMAIN, self._home.id) } - } + ) @property def icon(self) -> str: @@ -544,8 +544,8 @@ class HomematicipSecuritySensorGroup( @property def is_on(self) -> bool: """Return true if safety issue detected.""" - parent_is_on = super().is_on - if parent_is_on: + if super().is_on: + # parent is on return True if ( diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 1b6c2491e2e..ed91559e489 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -76,13 +76,13 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "identifiers": {(HMIPC_DOMAIN, self._device.id)}, - "name": self._device.label, - "manufacturer": "eQ-3", - "model": self._device.modelType, - "via_device": (HMIPC_DOMAIN, self._device.homeId), - } + return DeviceInfo( + identifiers={(HMIPC_DOMAIN, self._device.id)}, + manufacturer="eQ-3", + model=self._device.modelType, + name=self._device.label, + via_device=(HMIPC_DOMAIN, self._device.homeId), + ) @property def temperature_unit(self) -> str: @@ -207,8 +207,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return if self.min_temp <= temperature <= self.max_temp: diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index f1edff1854b..ecf0549d8b8 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -96,18 +96,18 @@ class HomematicipGenericEntity(Entity): """Return device specific attributes.""" # Only physical devices should be HA devices. if isinstance(self._device, AsyncDevice): - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers of Homematic IP device (HMIPC_DOMAIN, self._device.id) }, - "name": self._device.label, - "manufacturer": self._device.oem, - "model": self._device.modelType, - "sw_version": self._device.firmwareVersion, + manufacturer=self._device.oem, + model=self._device.modelType, + name=self._device.label, + sw_version=self._device.firmwareVersion, # Link to the homematic ip access point. - "via_device": (HMIPC_DOMAIN, self._device.homeId), - } + via_device=(HMIPC_DOMAIN, self._device.homeId), + ) return None async def async_added_to_hass(self) -> None: @@ -153,8 +153,7 @@ class HomematicipGenericEntity(Entity): if not self.registry_entry: return - device_id = self.registry_entry.device_id - if device_id: + if device_id := self.registry_entry.device_id: # Remove from device registry. device_registry = await dr.async_get_registry(self.hass) if device_id in device_registry.devices: @@ -163,8 +162,7 @@ class HomematicipGenericEntity(Entity): else: # Remove from entity registry. # Only relevant for entities that do not belong to a device. - entity_id = self.registry_entry.entity_id - if entity_id: + if entity_id := self.registry_entry.entity_id: entity_registry = await er.async_get_registry(self.hass) if entity_id in entity_registry.entities: entity_registry.async_remove(entity_id) diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index ae2bb9f0c6d..ae866bb42e2 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -26,13 +26,19 @@ from homematicip.aio.device import ( ) from homematicip.base.enums import ValveState -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, LENGTH_MILLIMETERS, LIGHT_LUX, PERCENTAGE, @@ -111,6 +117,7 @@ async def async_setup_entry( ), ): entities.append(HomematicipPowerSensor(hap, device)) + entities.append(HomematicipEnergySensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): @@ -127,6 +134,8 @@ async def async_setup_entry( class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity): """Representation of then HomeMaticIP access point.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize access point status entity.""" super().__init__(hap, device, post="Duty Cycle") @@ -179,6 +188,8 @@ class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity): class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP humidity sensor.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" super().__init__(hap, device, post="Humidity") @@ -202,6 +213,8 @@ class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity): class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP thermometer.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" super().__init__(hap, device, post="Temperature") @@ -239,6 +252,8 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP Illuminance sensor.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" super().__init__(hap, device, post="Illuminance") @@ -277,6 +292,8 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP power measuring sensor.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" super().__init__(hap, device, post="Power") @@ -297,6 +314,31 @@ class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity): return POWER_WATT +class HomematicipEnergySensor(HomematicipGenericEntity, SensorEntity): + """Representation of the HomematicIP energy measuring sensor.""" + + _attr_state_class = STATE_CLASS_TOTAL_INCREASING + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__(hap, device, post="Energy") + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return DEVICE_CLASS_ENERGY + + @property + def native_value(self) -> float: + """Return the energy counter value.""" + return self._device.energyCounter + + @property + def native_unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return ENERGY_KILO_WATT_HOUR + + class HomematicipWindspeedSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP wind speed sensor.""" diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index 45795f8858e..45b47b40efa 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -208,9 +208,8 @@ async def _async_activate_eco_mode_with_duration( ) -> None: """Service to activate eco mode with duration.""" duration = service.data[ATTR_DURATION] - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - if hapid: + if hapid := service.data.get(ATTR_ACCESSPOINT_ID): home = _get_home(hass, hapid) if home: await home.activate_absence_with_duration(duration) @@ -224,9 +223,8 @@ async def _async_activate_eco_mode_with_period( ) -> None: """Service to activate eco mode with period.""" endtime = service.data[ATTR_ENDTIME] - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - if hapid: + if hapid := service.data.get(ATTR_ACCESSPOINT_ID): home = _get_home(hass, hapid) if home: await home.activate_absence_with_period(endtime) @@ -239,9 +237,8 @@ async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> """Service to activate vacation.""" endtime = service.data[ATTR_ENDTIME] temperature = service.data[ATTR_TEMPERATURE] - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - if hapid: + if hapid := service.data.get(ATTR_ACCESSPOINT_ID): home = _get_home(hass, hapid) if home: await home.activate_vacation(endtime, temperature) @@ -252,9 +249,7 @@ async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) -> None: """Service to deactivate eco mode.""" - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: + if hapid := service.data.get(ATTR_ACCESSPOINT_ID): home = _get_home(hass, hapid) if home: await home.deactivate_absence() @@ -265,9 +260,7 @@ async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall) -> None: """Service to deactivate vacation.""" - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: + if hapid := service.data.get(ATTR_ACCESSPOINT_ID): home = _get_home(hass, hapid) if home: await home.deactivate_vacation() @@ -337,8 +330,7 @@ async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall) def _get_home(hass: HomeAssistant, hapid: str) -> AsyncHome | None: """Return a HmIP home.""" - hap = hass.data[HMIPC_DOMAIN].get(hapid) - if hap: + if hap := hass.data[HMIPC_DOMAIN].get(hapid): return hap.home _LOGGER.info("No matching access point found for access point id %s", hapid) diff --git a/homeassistant/components/homematicip_cloud/translations/he.json b/homeassistant/components/homematicip_cloud/translations/he.json index 55260a82f9d..8e6f13544b9 100644 --- a/homeassistant/components/homematicip_cloud/translations/he.json +++ b/homeassistant/components/homematicip_cloud/translations/he.json @@ -21,7 +21,7 @@ "title": "\u05d1\u05d7\u05e8 \u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 HomematicIP" }, "link": { - "description": "\u05dc\u05d7\u05e5 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05db\u05d7\u05d5\u05dc \u05d1\u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 \u05d5\u05e2\u05dc \u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05e9\u05dc\u05d9\u05d7\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d7\u05d1\u05e8 \u05d0\u05ea HomematicIP \u05e2\u05ddHome Assistant.\n\n![\u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d1\u05de\u05d2\u05e9\u05e8](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "\u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05db\u05d7\u05d5\u05dc \u05d1\u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 \u05d5\u05e2\u05dc \u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05e9\u05dc\u05d9\u05d7\u05d4 \u05db\u05d3\u05d9 \u05dc\u05e8\u05e9\u05d5\u05dd \u05d0\u05ea HomematIP \u05e2\u05dd Home Assistant.\n\n![\u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d1\u05de\u05d2\u05e9\u05e8](/static/images/config_flows/config_homematicip_cloud.png)", "title": "\u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 \u05dc\u05e7\u05d9\u05e9\u05d5\u05e8" } } diff --git a/homeassistant/components/homematicip_cloud/translations/ja.json b/homeassistant/components/homematicip_cloud/translations/ja.json index b26b247a66c..5b5d0d62ab9 100644 --- a/homeassistant/components/homematicip_cloud/translations/ja.json +++ b/homeassistant/components/homematicip_cloud/translations/ja.json @@ -6,7 +6,17 @@ }, "error": { "invalid_sgtin_or_pin": "PIN\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", - "press_the_button": "\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "press_the_button": "\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "register_failed": "\u767b\u9332\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "timeout_button": "\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3059\u3068\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3059\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "init": { + "data": { + "hapid": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8ID (SGTIN)", + "pin": "PIN\u30b3\u30fc\u30c9" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 8088a73506d..7c40a0bb684 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -282,8 +282,7 @@ class HoneywellUSThermostat(ClimateEntity): def _set_temperature(self, **kwargs) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return try: # Get current mode @@ -310,11 +309,9 @@ class HoneywellUSThermostat(ClimateEntity): try: if HVAC_MODE_HEAT_COOL in self._hvac_mode_map: - temperature = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if temperature: + if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): self._device.setpoint_cool = temperature - temperature = kwargs.get(ATTR_TARGET_TEMP_LOW) - if temperature: + if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): self._device.setpoint_heat = temperature except somecomfort.SomeComfortError as err: _LOGGER.error("Invalid temperature %s: %s", temperature, err) diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 5ba4947e046..a308a704c74 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -3,7 +3,7 @@ "name": "Honeywell Total Connect Comfort (US)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/honeywell", - "requirements": ["somecomfort==0.5.2"], + "requirements": ["somecomfort==0.7.0"], "codeowners": ["@rdfurman"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/honeywell/translations/bg.json b/homeassistant/components/honeywell/translations/bg.json new file mode 100644 index 00000000000..e7020268311 --- /dev/null +++ b/homeassistant/components/honeywell/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 594d84a8068..6c3a10757bb 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -186,9 +186,8 @@ def get_service(hass, config, discovery_info=None): hass.http.register_view(HTML5PushCallbackView(registrations)) gcm_api_key = config.get(ATTR_GCM_API_KEY) - gcm_sender_id = config.get(ATTR_GCM_SENDER_ID) - if gcm_sender_id is not None: + if config.get(ATTR_GCM_SENDER_ID) is not None: add_manifest_json_key(ATTR_GCM_SENDER_ID, config.get(ATTR_GCM_SENDER_ID)) return HTML5NotificationService( @@ -332,9 +331,7 @@ class HTML5PushCallbackView(HomeAssistantView): # https://auth0.com/docs/quickstart/backend/python def check_authorization_header(self, request): """Check the authorization header.""" - - auth = request.headers.get(AUTHORIZATION) - if not auth: + if not (auth := request.headers.get(AUTHORIZATION)): return self.json_message( "Authorization header is expected", status_code=HTTPStatus.UNAUTHORIZED ) @@ -463,9 +460,7 @@ class HTML5NotificationService(BaseNotificationService): ATTR_TITLE: kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), } - data = kwargs.get(ATTR_DATA) - - if data: + if data := kwargs.get(ATTR_DATA): # Pick out fields that should go into the notification directly vs # into the notification data dictionary. @@ -496,9 +491,8 @@ class HTML5NotificationService(BaseNotificationService): if priority not in ["normal", "high"]: priority = DEFAULT_PRIORITY payload["timestamp"] = timestamp * 1000 # Javascript ms since epoch - targets = kwargs.get(ATTR_TARGET) - if not targets: + if not (targets := kwargs.get(ATTR_TARGET)): targets = self.registrations.keys() for target in list(targets): diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 43ea0522594..e4d7da6ac9b 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -29,9 +29,7 @@ def async_sign_path( hass: HomeAssistant, refresh_token_id: str, path: str, expiration: timedelta ) -> str: """Sign a path for temporary access without auth header.""" - secret = hass.data.get(DATA_SIGN_SECRET) - - if secret is None: + if (secret := hass.data.get(DATA_SIGN_SECRET)) is None: secret = hass.data[DATA_SIGN_SECRET] = secrets.token_hex() now = dt_util.utcnow() @@ -80,14 +78,10 @@ def setup_auth(hass: HomeAssistant, app: Application) -> None: async def async_validate_signed_request(request: Request) -> bool: """Validate a signed request.""" - secret = hass.data.get(DATA_SIGN_SECRET) - - if secret is None: + if (secret := hass.data.get(DATA_SIGN_SECRET)) is None: return False - signature = request.query.get(SIGN_QUERY_PARAM) - - if signature is None: + if (signature := request.query.get(SIGN_QUERY_PARAM)) is None: return False try: diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 6e4f5c0a661..a1d50dbdcb5 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -5,6 +5,7 @@ from collections import defaultdict from collections.abc import Awaitable, Callable from contextlib import suppress from datetime import datetime +from http import HTTPStatus from ipaddress import ip_address import logging from socket import gethostbyaddr, herror @@ -15,7 +16,6 @@ from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized import voluptuous as vol from homeassistant.config import load_yaml_config_file -from homeassistant.const import HTTP_BAD_REQUEST from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -89,9 +89,9 @@ def log_invalid_auth( async def handle_req( view: HomeAssistantView, request: Request, *args: Any, **kwargs: Any ) -> StreamResponse: - """Try to log failed login attempts if response status >= 400.""" + """Try to log failed login attempts if response status >= BAD_REQUEST.""" resp = await func(view, request, *args, **kwargs) - if resp.status >= HTTP_BAD_REQUEST: + if resp.status >= HTTPStatus.BAD_REQUEST: await process_wrong_login(request) return resp diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 39225c918e5..bf8dc4b432b 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -19,7 +19,7 @@ from aiohttp.web_urldispatcher import AbstractRoute import voluptuous as vol from homeassistant import exceptions -from homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK, HTTP_SERVICE_UNAVAILABLE +from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import Context, is_callback from homeassistant.helpers.json import JSONEncoder @@ -40,8 +40,7 @@ class HomeAssistantView: @staticmethod def context(request: web.Request) -> Context: """Generate a context from a request.""" - user = request.get("hass_user") - if user is None: + if (user := request.get("hass_user")) is None: return Context() return Context(user_id=user.id) @@ -115,7 +114,7 @@ def request_handler_factory( async def handle(request: web.Request) -> web.StreamResponse: """Handle incoming request.""" if request.app[KEY_HASS].is_stopping: - return web.Response(status=HTTP_SERVICE_UNAVAILABLE) + return web.Response(status=HTTPStatus.SERVICE_UNAVAILABLE) authenticated = request.get(KEY_AUTHENTICATED, False) @@ -145,7 +144,7 @@ def request_handler_factory( # The method handler returned a ready-made Response, how nice of it return result - status_code = HTTP_OK + status_code = HTTPStatus.OK if isinstance(result, tuple): result, status_code = result diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 92122f1b2be..4b33f3e5a71 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -31,6 +31,8 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + ATTR_MODEL, + ATTR_SW_VERSION, CONF_MAC, CONF_NAME, CONF_PASSWORD, @@ -314,8 +316,7 @@ async def async_setup_entry( # noqa: C901 # Override settings from YAML config, but only if they're changed in it # Old values are stored as *_from_yaml in the config entry - yaml_config = hass.data[DOMAIN].config.get(url) - if yaml_config: + if yaml_config := hass.data[DOMAIN].config.get(url): # Config values new_data = {} for key in CONF_USERNAME, CONF_PASSWORD: @@ -372,10 +373,10 @@ async def async_setup_entry( # noqa: C901 await hass.async_add_executor_job(router.update) # Check that we found required information - device_info = router.data.get(KEY_DEVICE_INFORMATION) + router_info = router.data.get(KEY_DEVICE_INFORMATION) if not entry.unique_id: # Transitional from < 2021.8: update None config entry and entity unique ids - if device_info and (serial_number := device_info.get("SerialNumber")): + if router_info and (serial_number := router_info.get("SerialNumber")): hass.config_entries.async_update_entry(entry, unique_id=serial_number) ent_reg = entity_registry.async_get(hass) for entity_entry in entity_registry.async_entries_for_config_entry( @@ -420,35 +421,37 @@ async def async_setup_entry( # noqa: C901 except Exception: # pylint: disable=broad-except # Assume not supported, or authentication required but in unauthenticated mode wlan_settings = {} - macs = get_device_macs(device_info or {}, wlan_settings) + macs = get_device_macs(router_info or {}, wlan_settings) # Be careful not to overwrite a previous, more complete set with a partial one - if macs and (not entry.data[CONF_MAC] or (device_info and wlan_settings)): + if macs and (not entry.data[CONF_MAC] or (router_info and wlan_settings)): new_data = dict(entry.data) new_data[CONF_MAC] = macs hass.config_entries.async_update_entry(entry, data=new_data) # Set up device registry if router.device_identifiers or router.device_connections: - device_data = {} + device_info = DeviceInfo( + configuration_url=router.url, + connections=router.device_connections, + identifiers=router.device_identifiers, + name=router.device_name, + manufacturer="Huawei", + ) sw_version = None - if device_info: - sw_version = device_info.get("SoftwareVersion") - if device_info.get("DeviceName"): - device_data["model"] = device_info["DeviceName"] + if router_info: + sw_version = router_info.get("SoftwareVersion") + if router_info.get("DeviceName"): + device_info[ATTR_MODEL] = router_info["DeviceName"] if not sw_version and router.data.get(KEY_DEVICE_BASIC_INFORMATION): sw_version = router.data[KEY_DEVICE_BASIC_INFORMATION].get( "SoftwareVersion" ) if sw_version: - device_data["sw_version"] = sw_version + device_info[ATTR_SW_VERSION] = sw_version device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections=router.device_connections, - identifiers=router.device_identifiers, - name=router.device_name, - manufacturer="Huawei", - **device_data, + **device_info, ) # Forward config entry setup to platforms @@ -652,10 +655,10 @@ class HuaweiLteBaseEntity(Entity): @property def device_info(self) -> DeviceInfo: """Get info for matching with parent router.""" - return { - "identifiers": self.router.device_identifiers, - "connections": self.router.device_connections, - } + return DeviceInfo( + connections=self.router.device_connections, + identifiers=self.router.device_identifiers, + ) async def async_update(self) -> None: """Update state.""" diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 568f7c31a53..4479f383524 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -249,9 +249,12 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { name="Battery", device_class=DEVICE_CLASS_BATTERY, unit=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), (KEY_MONITORING_STATUS, "CurrentWifiUser"): SensorMeta( - name="WiFi clients connected", icon="mdi:wifi" + name="WiFi clients connected", + icon="mdi:wifi", + state_class=STATE_CLASS_MEASUREMENT, ), (KEY_MONITORING_STATUS, "PrimaryDns"): SensorMeta( name="Primary DNS server", icon="mdi:ip" @@ -296,7 +299,10 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { state_class=STATE_CLASS_MEASUREMENT, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): SensorMeta( - name="Total connected duration", unit=TIME_SECONDS, icon="mdi:timer-outline" + name="Total connected duration", + unit=TIME_SECONDS, + icon="mdi:timer-outline", + state_class=STATE_CLASS_TOTAL_INCREASING, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): SensorMeta( name="Total download", diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index 91f70a17e46..36d08438fca 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z" }, "error": { diff --git a/homeassistant/components/huawei_router/__init__.py b/homeassistant/components/huawei_router/__init__.py deleted file mode 100644 index 861809992c6..00000000000 --- a/homeassistant/components/huawei_router/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The huawei_router component.""" diff --git a/homeassistant/components/huawei_router/device_tracker.py b/homeassistant/components/huawei_router/device_tracker.py deleted file mode 100644 index d4882f0a499..00000000000 --- a/homeassistant/components/huawei_router/device_tracker.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Support for HUAWEI routers.""" -import base64 -from collections import namedtuple -import logging -import re - -import requests -import voluptuous as vol - -from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - DeviceScanner, -) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - } -) - - -def get_scanner(hass, config): - """Validate the configuration and return a HUAWEI scanner.""" - scanner = HuaweiDeviceScanner(config[DOMAIN]) - - return scanner - - -Device = namedtuple("Device", ["name", "ip", "mac", "state"]) - - -class HuaweiDeviceScanner(DeviceScanner): - """This class queries a router running HUAWEI firmware.""" - - ARRAY_REGEX = re.compile(r"var UserDevinfo = new Array\((.*)null\);") - DEVICE_REGEX = re.compile(r"new USERDevice\((.*?)\),") - DEVICE_ATTR_REGEX = re.compile( - '"(?P.*?)","(?P.*?)",' - '"(?P.*?)","(?P.*?)",' - '"(?P.*?)","(?P.*?)",' - '"(?P.*?)","(?P.*?)",' - '"(?P