mirror of
https://github.com/home-assistant/core.git
synced 2025-07-09 22:37:11 +00:00
2024.1.0 (#106970)
This commit is contained in:
commit
fb0cc6c5d0
43
.coveragerc
43
.coveragerc
@ -173,6 +173,7 @@ omit =
|
|||||||
homeassistant/components/coinbase/sensor.py
|
homeassistant/components/coinbase/sensor.py
|
||||||
homeassistant/components/comed_hourly_pricing/sensor.py
|
homeassistant/components/comed_hourly_pricing/sensor.py
|
||||||
homeassistant/components/comelit/__init__.py
|
homeassistant/components/comelit/__init__.py
|
||||||
|
homeassistant/components/comelit/alarm_control_panel.py
|
||||||
homeassistant/components/comelit/const.py
|
homeassistant/components/comelit/const.py
|
||||||
homeassistant/components/comelit/cover.py
|
homeassistant/components/comelit/cover.py
|
||||||
homeassistant/components/comelit/coordinator.py
|
homeassistant/components/comelit/coordinator.py
|
||||||
@ -337,7 +338,6 @@ omit =
|
|||||||
homeassistant/components/escea/__init__.py
|
homeassistant/components/escea/__init__.py
|
||||||
homeassistant/components/escea/climate.py
|
homeassistant/components/escea/climate.py
|
||||||
homeassistant/components/escea/discovery.py
|
homeassistant/components/escea/discovery.py
|
||||||
homeassistant/components/esphome/bluetooth/*
|
|
||||||
homeassistant/components/esphome/manager.py
|
homeassistant/components/esphome/manager.py
|
||||||
homeassistant/components/etherscan/sensor.py
|
homeassistant/components/etherscan/sensor.py
|
||||||
homeassistant/components/eufy/*
|
homeassistant/components/eufy/*
|
||||||
@ -364,8 +364,6 @@ omit =
|
|||||||
homeassistant/components/faa_delays/binary_sensor.py
|
homeassistant/components/faa_delays/binary_sensor.py
|
||||||
homeassistant/components/faa_delays/coordinator.py
|
homeassistant/components/faa_delays/coordinator.py
|
||||||
homeassistant/components/familyhub/camera.py
|
homeassistant/components/familyhub/camera.py
|
||||||
homeassistant/components/fastdotcom/sensor.py
|
|
||||||
homeassistant/components/fastdotcom/__init__.py
|
|
||||||
homeassistant/components/ffmpeg/camera.py
|
homeassistant/components/ffmpeg/camera.py
|
||||||
homeassistant/components/fibaro/__init__.py
|
homeassistant/components/fibaro/__init__.py
|
||||||
homeassistant/components/fibaro/binary_sensor.py
|
homeassistant/components/fibaro/binary_sensor.py
|
||||||
@ -404,6 +402,9 @@ omit =
|
|||||||
homeassistant/components/fjaraskupan/sensor.py
|
homeassistant/components/fjaraskupan/sensor.py
|
||||||
homeassistant/components/fleetgo/device_tracker.py
|
homeassistant/components/fleetgo/device_tracker.py
|
||||||
homeassistant/components/flexit/climate.py
|
homeassistant/components/flexit/climate.py
|
||||||
|
homeassistant/components/flexit_bacnet/__init__.py
|
||||||
|
homeassistant/components/flexit_bacnet/const.py
|
||||||
|
homeassistant/components/flexit_bacnet/climate.py
|
||||||
homeassistant/components/flic/binary_sensor.py
|
homeassistant/components/flic/binary_sensor.py
|
||||||
homeassistant/components/flick_electric/__init__.py
|
homeassistant/components/flick_electric/__init__.py
|
||||||
homeassistant/components/flick_electric/sensor.py
|
homeassistant/components/flick_electric/sensor.py
|
||||||
@ -419,6 +420,7 @@ omit =
|
|||||||
homeassistant/components/fortios/device_tracker.py
|
homeassistant/components/fortios/device_tracker.py
|
||||||
homeassistant/components/foscam/__init__.py
|
homeassistant/components/foscam/__init__.py
|
||||||
homeassistant/components/foscam/camera.py
|
homeassistant/components/foscam/camera.py
|
||||||
|
homeassistant/components/foscam/coordinator.py
|
||||||
homeassistant/components/foursquare/*
|
homeassistant/components/foursquare/*
|
||||||
homeassistant/components/free_mobile/notify.py
|
homeassistant/components/free_mobile/notify.py
|
||||||
homeassistant/components/freebox/camera.py
|
homeassistant/components/freebox/camera.py
|
||||||
@ -536,6 +538,7 @@ omit =
|
|||||||
homeassistant/components/hvv_departures/binary_sensor.py
|
homeassistant/components/hvv_departures/binary_sensor.py
|
||||||
homeassistant/components/hvv_departures/sensor.py
|
homeassistant/components/hvv_departures/sensor.py
|
||||||
homeassistant/components/ialarm/alarm_control_panel.py
|
homeassistant/components/ialarm/alarm_control_panel.py
|
||||||
|
homeassistant/components/iammeter/const.py
|
||||||
homeassistant/components/iammeter/sensor.py
|
homeassistant/components/iammeter/sensor.py
|
||||||
homeassistant/components/iaqualink/binary_sensor.py
|
homeassistant/components/iaqualink/binary_sensor.py
|
||||||
homeassistant/components/iaqualink/climate.py
|
homeassistant/components/iaqualink/climate.py
|
||||||
@ -754,6 +757,9 @@ omit =
|
|||||||
homeassistant/components/motion_blinds/cover.py
|
homeassistant/components/motion_blinds/cover.py
|
||||||
homeassistant/components/motion_blinds/entity.py
|
homeassistant/components/motion_blinds/entity.py
|
||||||
homeassistant/components/motion_blinds/sensor.py
|
homeassistant/components/motion_blinds/sensor.py
|
||||||
|
homeassistant/components/motionmount/__init__.py
|
||||||
|
homeassistant/components/motionmount/entity.py
|
||||||
|
homeassistant/components/motionmount/number.py
|
||||||
homeassistant/components/mpd/media_player.py
|
homeassistant/components/mpd/media_player.py
|
||||||
homeassistant/components/mqtt_room/sensor.py
|
homeassistant/components/mqtt_room/sensor.py
|
||||||
homeassistant/components/msteams/notify.py
|
homeassistant/components/msteams/notify.py
|
||||||
@ -799,7 +805,8 @@ omit =
|
|||||||
homeassistant/components/netgear/sensor.py
|
homeassistant/components/netgear/sensor.py
|
||||||
homeassistant/components/netgear/switch.py
|
homeassistant/components/netgear/switch.py
|
||||||
homeassistant/components/netgear/update.py
|
homeassistant/components/netgear/update.py
|
||||||
homeassistant/components/netgear_lte/*
|
homeassistant/components/netgear_lte/__init__.py
|
||||||
|
homeassistant/components/netgear_lte/notify.py
|
||||||
homeassistant/components/netio/switch.py
|
homeassistant/components/netio/switch.py
|
||||||
homeassistant/components/neurio_energy/sensor.py
|
homeassistant/components/neurio_energy/sensor.py
|
||||||
homeassistant/components/nexia/climate.py
|
homeassistant/components/nexia/climate.py
|
||||||
@ -900,6 +907,9 @@ omit =
|
|||||||
homeassistant/components/opple/light.py
|
homeassistant/components/opple/light.py
|
||||||
homeassistant/components/oru/*
|
homeassistant/components/oru/*
|
||||||
homeassistant/components/orvibo/switch.py
|
homeassistant/components/orvibo/switch.py
|
||||||
|
homeassistant/components/osoenergy/__init__.py
|
||||||
|
homeassistant/components/osoenergy/const.py
|
||||||
|
homeassistant/components/osoenergy/water_heater.py
|
||||||
homeassistant/components/osramlightify/light.py
|
homeassistant/components/osramlightify/light.py
|
||||||
homeassistant/components/otp/sensor.py
|
homeassistant/components/otp/sensor.py
|
||||||
homeassistant/components/overkiz/__init__.py
|
homeassistant/components/overkiz/__init__.py
|
||||||
@ -1026,6 +1036,12 @@ omit =
|
|||||||
homeassistant/components/recorder/repack.py
|
homeassistant/components/recorder/repack.py
|
||||||
homeassistant/components/recswitch/switch.py
|
homeassistant/components/recswitch/switch.py
|
||||||
homeassistant/components/reddit/sensor.py
|
homeassistant/components/reddit/sensor.py
|
||||||
|
homeassistant/components/refoss/__init__.py
|
||||||
|
homeassistant/components/refoss/bridge.py
|
||||||
|
homeassistant/components/refoss/coordinator.py
|
||||||
|
homeassistant/components/refoss/entity.py
|
||||||
|
homeassistant/components/refoss/switch.py
|
||||||
|
homeassistant/components/refoss/util.py
|
||||||
homeassistant/components/rejseplanen/sensor.py
|
homeassistant/components/rejseplanen/sensor.py
|
||||||
homeassistant/components/remember_the_milk/__init__.py
|
homeassistant/components/remember_the_milk/__init__.py
|
||||||
homeassistant/components/remote_rpi_gpio/*
|
homeassistant/components/remote_rpi_gpio/*
|
||||||
@ -1209,6 +1225,7 @@ omit =
|
|||||||
homeassistant/components/starline/__init__.py
|
homeassistant/components/starline/__init__.py
|
||||||
homeassistant/components/starline/account.py
|
homeassistant/components/starline/account.py
|
||||||
homeassistant/components/starline/binary_sensor.py
|
homeassistant/components/starline/binary_sensor.py
|
||||||
|
homeassistant/components/starline/button.py
|
||||||
homeassistant/components/starline/device_tracker.py
|
homeassistant/components/starline/device_tracker.py
|
||||||
homeassistant/components/starline/entity.py
|
homeassistant/components/starline/entity.py
|
||||||
homeassistant/components/starline/lock.py
|
homeassistant/components/starline/lock.py
|
||||||
@ -1226,8 +1243,12 @@ omit =
|
|||||||
homeassistant/components/stream/fmp4utils.py
|
homeassistant/components/stream/fmp4utils.py
|
||||||
homeassistant/components/stream/hls.py
|
homeassistant/components/stream/hls.py
|
||||||
homeassistant/components/stream/worker.py
|
homeassistant/components/stream/worker.py
|
||||||
homeassistant/components/streamlabswater/*
|
homeassistant/components/streamlabswater/__init__.py
|
||||||
homeassistant/components/suez_water/*
|
homeassistant/components/streamlabswater/binary_sensor.py
|
||||||
|
homeassistant/components/streamlabswater/coordinator.py
|
||||||
|
homeassistant/components/streamlabswater/sensor.py
|
||||||
|
homeassistant/components/suez_water/__init__.py
|
||||||
|
homeassistant/components/suez_water/sensor.py
|
||||||
homeassistant/components/supervisord/sensor.py
|
homeassistant/components/supervisord/sensor.py
|
||||||
homeassistant/components/supla/*
|
homeassistant/components/supla/*
|
||||||
homeassistant/components/surepetcare/__init__.py
|
homeassistant/components/surepetcare/__init__.py
|
||||||
@ -1235,6 +1256,8 @@ omit =
|
|||||||
homeassistant/components/surepetcare/entity.py
|
homeassistant/components/surepetcare/entity.py
|
||||||
homeassistant/components/surepetcare/sensor.py
|
homeassistant/components/surepetcare/sensor.py
|
||||||
homeassistant/components/swiss_hydrological_data/sensor.py
|
homeassistant/components/swiss_hydrological_data/sensor.py
|
||||||
|
homeassistant/components/swiss_public_transport/__init__.py
|
||||||
|
homeassistant/components/swiss_public_transport/coordinator.py
|
||||||
homeassistant/components/swiss_public_transport/sensor.py
|
homeassistant/components/swiss_public_transport/sensor.py
|
||||||
homeassistant/components/swisscom/device_tracker.py
|
homeassistant/components/swisscom/device_tracker.py
|
||||||
homeassistant/components/switchbee/__init__.py
|
homeassistant/components/switchbee/__init__.py
|
||||||
@ -1286,7 +1309,9 @@ omit =
|
|||||||
homeassistant/components/system_bridge/notify.py
|
homeassistant/components/system_bridge/notify.py
|
||||||
homeassistant/components/system_bridge/sensor.py
|
homeassistant/components/system_bridge/sensor.py
|
||||||
homeassistant/components/system_bridge/update.py
|
homeassistant/components/system_bridge/update.py
|
||||||
|
homeassistant/components/systemmonitor/__init__.py
|
||||||
homeassistant/components/systemmonitor/sensor.py
|
homeassistant/components/systemmonitor/sensor.py
|
||||||
|
homeassistant/components/systemmonitor/util.py
|
||||||
homeassistant/components/tado/__init__.py
|
homeassistant/components/tado/__init__.py
|
||||||
homeassistant/components/tado/binary_sensor.py
|
homeassistant/components/tado/binary_sensor.py
|
||||||
homeassistant/components/tado/climate.py
|
homeassistant/components/tado/climate.py
|
||||||
@ -1375,10 +1400,6 @@ omit =
|
|||||||
homeassistant/components/tradfri/light.py
|
homeassistant/components/tradfri/light.py
|
||||||
homeassistant/components/tradfri/sensor.py
|
homeassistant/components/tradfri/sensor.py
|
||||||
homeassistant/components/tradfri/switch.py
|
homeassistant/components/tradfri/switch.py
|
||||||
homeassistant/components/trafikverket_train/__init__.py
|
|
||||||
homeassistant/components/trafikverket_train/coordinator.py
|
|
||||||
homeassistant/components/trafikverket_train/sensor.py
|
|
||||||
homeassistant/components/trafikverket_train/util.py
|
|
||||||
homeassistant/components/trafikverket_weatherstation/__init__.py
|
homeassistant/components/trafikverket_weatherstation/__init__.py
|
||||||
homeassistant/components/trafikverket_weatherstation/coordinator.py
|
homeassistant/components/trafikverket_weatherstation/coordinator.py
|
||||||
homeassistant/components/trafikverket_weatherstation/sensor.py
|
homeassistant/components/trafikverket_weatherstation/sensor.py
|
||||||
@ -1413,6 +1434,8 @@ omit =
|
|||||||
homeassistant/components/ukraine_alarm/__init__.py
|
homeassistant/components/ukraine_alarm/__init__.py
|
||||||
homeassistant/components/ukraine_alarm/binary_sensor.py
|
homeassistant/components/ukraine_alarm/binary_sensor.py
|
||||||
homeassistant/components/unifiled/*
|
homeassistant/components/unifiled/*
|
||||||
|
homeassistant/components/unifi_direct/__init__.py
|
||||||
|
homeassistant/components/unifi_direct/device_tracker.py
|
||||||
homeassistant/components/upb/__init__.py
|
homeassistant/components/upb/__init__.py
|
||||||
homeassistant/components/upb/light.py
|
homeassistant/components/upb/light.py
|
||||||
homeassistant/components/upc_connect/*
|
homeassistant/components/upc_connect/*
|
||||||
|
8
.github/workflows/builder.yml
vendored
8
.github/workflows/builder.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@ -331,7 +331,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.2.0
|
uses: sigstore/cosign-installer@v3.3.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.0.2"
|
cosign-release: "v2.0.2"
|
||||||
|
|
||||||
|
26
.github/workflows/ci.yaml
vendored
26
.github/workflows/ci.yaml
vendored
@ -36,7 +36,7 @@ env:
|
|||||||
CACHE_VERSION: 5
|
CACHE_VERSION: 5
|
||||||
PIP_CACHE_VERSION: 4
|
PIP_CACHE_VERSION: 4
|
||||||
MYPY_CACHE_VERSION: 6
|
MYPY_CACHE_VERSION: 6
|
||||||
HA_SHORT_VERSION: "2023.12"
|
HA_SHORT_VERSION: "2024.1"
|
||||||
DEFAULT_PYTHON: "3.11"
|
DEFAULT_PYTHON: "3.11"
|
||||||
ALL_PYTHON_VERSIONS: "['3.11', '3.12']"
|
ALL_PYTHON_VERSIONS: "['3.11', '3.12']"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
@ -225,7 +225,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -269,7 +269,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -309,7 +309,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -348,7 +348,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -443,7 +443,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -511,7 +511,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -543,7 +543,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -576,7 +576,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -620,7 +620,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -702,7 +702,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -854,7 +854,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -978,7 +978,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -29,11 +29,11 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2.22.8
|
uses: github/codeql-action/init@v3.22.12
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2.22.8
|
uses: github/codeql-action/analyze@v3.22.12
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
18
.github/workflows/stale.yml
vendored
18
.github/workflows/stale.yml
vendored
@ -11,16 +11,16 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# The 90 day stale policy for PRs
|
# The 60 day stale policy for PRs
|
||||||
# Used for:
|
# Used for:
|
||||||
# - PRs
|
# - PRs
|
||||||
# - No PRs marked as no-stale
|
# - No PRs marked as no-stale
|
||||||
# - No issues (-1)
|
# - No issues (-1)
|
||||||
- name: 90 days stale PRs policy
|
- name: 60 days stale PRs policy
|
||||||
uses: actions/stale@v8.0.0
|
uses: actions/stale@v9.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 90
|
days-before-stale: 60
|
||||||
days-before-close: 7
|
days-before-close: 7
|
||||||
days-before-issue-stale: -1
|
days-before-issue-stale: -1
|
||||||
days-before-issue-close: -1
|
days-before-issue-close: -1
|
||||||
@ -33,7 +33,11 @@ jobs:
|
|||||||
pull request has been automatically marked as stale because of that
|
pull request has been automatically marked as stale because of that
|
||||||
and will be closed if no further activity occurs within 7 days.
|
and will be closed if no further activity occurs within 7 days.
|
||||||
|
|
||||||
Thank you for your contributions.
|
If you are the author of this PR, please leave a comment if you want
|
||||||
|
to keep it open. Also, please rebase your PR onto the latest dev
|
||||||
|
branch to ensure that it's up to date with the latest changes.
|
||||||
|
|
||||||
|
Thank you for your contribution!
|
||||||
|
|
||||||
# Generate a token for the GitHub App, we use this method to avoid
|
# Generate a token for the GitHub App, we use this method to avoid
|
||||||
# hitting API limits for our GitHub actions + have a higher rate limit.
|
# hitting API limits for our GitHub actions + have a higher rate limit.
|
||||||
@ -53,7 +57,7 @@ jobs:
|
|||||||
# - No issues marked as no-stale or help-wanted
|
# - No issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: 90 days stale issues
|
- name: 90 days stale issues
|
||||||
uses: actions/stale@v8.0.0
|
uses: actions/stale@v9.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
@ -83,7 +87,7 @@ jobs:
|
|||||||
# - No Issues marked as no-stale or help-wanted
|
# - No Issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: Needs more information stale issues policy
|
- name: Needs more information stale issues policy
|
||||||
uses: actions/stale@v8.0.0
|
uses: actions/stale@v9.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
only-labels: "needs-more-information"
|
only-labels: "needs-more-information"
|
||||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v4.7.1
|
uses: actions/setup-python@v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.1.6
|
rev: v0.1.8
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
|
@ -43,11 +43,14 @@ homeassistant.components.abode.*
|
|||||||
homeassistant.components.accuweather.*
|
homeassistant.components.accuweather.*
|
||||||
homeassistant.components.acer_projector.*
|
homeassistant.components.acer_projector.*
|
||||||
homeassistant.components.actiontec.*
|
homeassistant.components.actiontec.*
|
||||||
|
homeassistant.components.adax.*
|
||||||
homeassistant.components.adguard.*
|
homeassistant.components.adguard.*
|
||||||
homeassistant.components.aftership.*
|
homeassistant.components.aftership.*
|
||||||
homeassistant.components.air_quality.*
|
homeassistant.components.air_quality.*
|
||||||
homeassistant.components.airly.*
|
homeassistant.components.airly.*
|
||||||
|
homeassistant.components.airnow.*
|
||||||
homeassistant.components.airvisual.*
|
homeassistant.components.airvisual.*
|
||||||
|
homeassistant.components.airvisual_pro.*
|
||||||
homeassistant.components.airzone.*
|
homeassistant.components.airzone.*
|
||||||
homeassistant.components.airzone_cloud.*
|
homeassistant.components.airzone_cloud.*
|
||||||
homeassistant.components.aladdin_connect.*
|
homeassistant.components.aladdin_connect.*
|
||||||
@ -59,10 +62,14 @@ homeassistant.components.ambient_station.*
|
|||||||
homeassistant.components.amcrest.*
|
homeassistant.components.amcrest.*
|
||||||
homeassistant.components.ampio.*
|
homeassistant.components.ampio.*
|
||||||
homeassistant.components.analytics.*
|
homeassistant.components.analytics.*
|
||||||
|
homeassistant.components.android_ip_webcam.*
|
||||||
|
homeassistant.components.androidtv_remote.*
|
||||||
homeassistant.components.anova.*
|
homeassistant.components.anova.*
|
||||||
homeassistant.components.anthemav.*
|
homeassistant.components.anthemav.*
|
||||||
homeassistant.components.apcupsd.*
|
homeassistant.components.apcupsd.*
|
||||||
|
homeassistant.components.apprise.*
|
||||||
homeassistant.components.aqualogic.*
|
homeassistant.components.aqualogic.*
|
||||||
|
homeassistant.components.aranet.*
|
||||||
homeassistant.components.aseko_pool_live.*
|
homeassistant.components.aseko_pool_live.*
|
||||||
homeassistant.components.assist_pipeline.*
|
homeassistant.components.assist_pipeline.*
|
||||||
homeassistant.components.asuswrt.*
|
homeassistant.components.asuswrt.*
|
||||||
@ -75,6 +82,7 @@ homeassistant.components.bayesian.*
|
|||||||
homeassistant.components.binary_sensor.*
|
homeassistant.components.binary_sensor.*
|
||||||
homeassistant.components.bitcoin.*
|
homeassistant.components.bitcoin.*
|
||||||
homeassistant.components.blockchain.*
|
homeassistant.components.blockchain.*
|
||||||
|
homeassistant.components.blue_current.*
|
||||||
homeassistant.components.bluetooth.*
|
homeassistant.components.bluetooth.*
|
||||||
homeassistant.components.bluetooth_tracker.*
|
homeassistant.components.bluetooth_tracker.*
|
||||||
homeassistant.components.bmw_connected_drive.*
|
homeassistant.components.bmw_connected_drive.*
|
||||||
@ -117,9 +125,12 @@ homeassistant.components.elgato.*
|
|||||||
homeassistant.components.elkm1.*
|
homeassistant.components.elkm1.*
|
||||||
homeassistant.components.emulated_hue.*
|
homeassistant.components.emulated_hue.*
|
||||||
homeassistant.components.energy.*
|
homeassistant.components.energy.*
|
||||||
|
homeassistant.components.enigma2.*
|
||||||
homeassistant.components.esphome.*
|
homeassistant.components.esphome.*
|
||||||
homeassistant.components.event.*
|
homeassistant.components.event.*
|
||||||
homeassistant.components.evil_genius_labs.*
|
homeassistant.components.evil_genius_labs.*
|
||||||
|
homeassistant.components.evohome.*
|
||||||
|
homeassistant.components.faa_delays.*
|
||||||
homeassistant.components.fan.*
|
homeassistant.components.fan.*
|
||||||
homeassistant.components.fastdotcom.*
|
homeassistant.components.fastdotcom.*
|
||||||
homeassistant.components.feedreader.*
|
homeassistant.components.feedreader.*
|
||||||
@ -127,6 +138,7 @@ homeassistant.components.file_upload.*
|
|||||||
homeassistant.components.filesize.*
|
homeassistant.components.filesize.*
|
||||||
homeassistant.components.filter.*
|
homeassistant.components.filter.*
|
||||||
homeassistant.components.fitbit.*
|
homeassistant.components.fitbit.*
|
||||||
|
homeassistant.components.flexit_bacnet.*
|
||||||
homeassistant.components.flux_led.*
|
homeassistant.components.flux_led.*
|
||||||
homeassistant.components.forecast_solar.*
|
homeassistant.components.forecast_solar.*
|
||||||
homeassistant.components.fritz.*
|
homeassistant.components.fritz.*
|
||||||
@ -150,6 +162,7 @@ homeassistant.components.hardkernel.*
|
|||||||
homeassistant.components.hardware.*
|
homeassistant.components.hardware.*
|
||||||
homeassistant.components.here_travel_time.*
|
homeassistant.components.here_travel_time.*
|
||||||
homeassistant.components.history.*
|
homeassistant.components.history.*
|
||||||
|
homeassistant.components.holiday.*
|
||||||
homeassistant.components.homeassistant.exposed_entities
|
homeassistant.components.homeassistant.exposed_entities
|
||||||
homeassistant.components.homeassistant.triggers.event
|
homeassistant.components.homeassistant.triggers.event
|
||||||
homeassistant.components.homeassistant_alerts.*
|
homeassistant.components.homeassistant_alerts.*
|
||||||
@ -228,6 +241,7 @@ homeassistant.components.modbus.*
|
|||||||
homeassistant.components.modem_callerid.*
|
homeassistant.components.modem_callerid.*
|
||||||
homeassistant.components.moon.*
|
homeassistant.components.moon.*
|
||||||
homeassistant.components.mopeka.*
|
homeassistant.components.mopeka.*
|
||||||
|
homeassistant.components.motionmount.*
|
||||||
homeassistant.components.mqtt.*
|
homeassistant.components.mqtt.*
|
||||||
homeassistant.components.mysensors.*
|
homeassistant.components.mysensors.*
|
||||||
homeassistant.components.nam.*
|
homeassistant.components.nam.*
|
||||||
@ -264,6 +278,7 @@ homeassistant.components.proximity.*
|
|||||||
homeassistant.components.prusalink.*
|
homeassistant.components.prusalink.*
|
||||||
homeassistant.components.pure_energie.*
|
homeassistant.components.pure_energie.*
|
||||||
homeassistant.components.purpleair.*
|
homeassistant.components.purpleair.*
|
||||||
|
homeassistant.components.pushbullet.*
|
||||||
homeassistant.components.pvoutput.*
|
homeassistant.components.pvoutput.*
|
||||||
homeassistant.components.qnap_qsw.*
|
homeassistant.components.qnap_qsw.*
|
||||||
homeassistant.components.radarr.*
|
homeassistant.components.radarr.*
|
||||||
@ -313,6 +328,8 @@ homeassistant.components.statistics.*
|
|||||||
homeassistant.components.steamist.*
|
homeassistant.components.steamist.*
|
||||||
homeassistant.components.stookalert.*
|
homeassistant.components.stookalert.*
|
||||||
homeassistant.components.stream.*
|
homeassistant.components.stream.*
|
||||||
|
homeassistant.components.streamlabswater.*
|
||||||
|
homeassistant.components.suez_water.*
|
||||||
homeassistant.components.sun.*
|
homeassistant.components.sun.*
|
||||||
homeassistant.components.surepetcare.*
|
homeassistant.components.surepetcare.*
|
||||||
homeassistant.components.switch.*
|
homeassistant.components.switch.*
|
||||||
@ -323,6 +340,7 @@ homeassistant.components.synology_dsm.*
|
|||||||
homeassistant.components.systemmonitor.*
|
homeassistant.components.systemmonitor.*
|
||||||
homeassistant.components.tag.*
|
homeassistant.components.tag.*
|
||||||
homeassistant.components.tailscale.*
|
homeassistant.components.tailscale.*
|
||||||
|
homeassistant.components.tailwind.*
|
||||||
homeassistant.components.tami4.*
|
homeassistant.components.tami4.*
|
||||||
homeassistant.components.tautulli.*
|
homeassistant.components.tautulli.*
|
||||||
homeassistant.components.tcp.*
|
homeassistant.components.tcp.*
|
||||||
@ -353,6 +371,7 @@ homeassistant.components.uptimerobot.*
|
|||||||
homeassistant.components.usb.*
|
homeassistant.components.usb.*
|
||||||
homeassistant.components.vacuum.*
|
homeassistant.components.vacuum.*
|
||||||
homeassistant.components.vallox.*
|
homeassistant.components.vallox.*
|
||||||
|
homeassistant.components.valve.*
|
||||||
homeassistant.components.velbus.*
|
homeassistant.components.velbus.*
|
||||||
homeassistant.components.vlc_telnet.*
|
homeassistant.components.vlc_telnet.*
|
||||||
homeassistant.components.wake_on_lan.*
|
homeassistant.components.wake_on_lan.*
|
||||||
|
62
CODEOWNERS
62
CODEOWNERS
@ -86,6 +86,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/anova/ @Lash-L
|
/tests/components/anova/ @Lash-L
|
||||||
/homeassistant/components/anthemav/ @hyralex
|
/homeassistant/components/anthemav/ @hyralex
|
||||||
/tests/components/anthemav/ @hyralex
|
/tests/components/anthemav/ @hyralex
|
||||||
|
/homeassistant/components/aosmith/ @bdr99
|
||||||
|
/tests/components/aosmith/ @bdr99
|
||||||
/homeassistant/components/apache_kafka/ @bachya
|
/homeassistant/components/apache_kafka/ @bachya
|
||||||
/tests/components/apache_kafka/ @bachya
|
/tests/components/apache_kafka/ @bachya
|
||||||
/homeassistant/components/apcupsd/ @yuxincs
|
/homeassistant/components/apcupsd/ @yuxincs
|
||||||
@ -153,6 +155,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/blebox/ @bbx-a @riokuu
|
/tests/components/blebox/ @bbx-a @riokuu
|
||||||
/homeassistant/components/blink/ @fronzbot @mkmer
|
/homeassistant/components/blink/ @fronzbot @mkmer
|
||||||
/tests/components/blink/ @fronzbot @mkmer
|
/tests/components/blink/ @fronzbot @mkmer
|
||||||
|
/homeassistant/components/blue_current/ @Floris272 @gleeuwen
|
||||||
|
/tests/components/blue_current/ @Floris272 @gleeuwen
|
||||||
/homeassistant/components/bluemaestro/ @bdraco
|
/homeassistant/components/bluemaestro/ @bdraco
|
||||||
/tests/components/bluemaestro/ @bdraco
|
/tests/components/bluemaestro/ @bdraco
|
||||||
/homeassistant/components/blueprint/ @home-assistant/core
|
/homeassistant/components/blueprint/ @home-assistant/core
|
||||||
@ -193,6 +197,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/camera/ @home-assistant/core
|
/tests/components/camera/ @home-assistant/core
|
||||||
/homeassistant/components/cast/ @emontnemery
|
/homeassistant/components/cast/ @emontnemery
|
||||||
/tests/components/cast/ @emontnemery
|
/tests/components/cast/ @emontnemery
|
||||||
|
/homeassistant/components/ccm15/ @ocalvo
|
||||||
|
/tests/components/ccm15/ @ocalvo
|
||||||
/homeassistant/components/cert_expiry/ @jjlawren
|
/homeassistant/components/cert_expiry/ @jjlawren
|
||||||
/tests/components/cert_expiry/ @jjlawren
|
/tests/components/cert_expiry/ @jjlawren
|
||||||
/homeassistant/components/circuit/ @braam
|
/homeassistant/components/circuit/ @braam
|
||||||
@ -205,8 +211,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/cloud/ @home-assistant/cloud
|
/tests/components/cloud/ @home-assistant/cloud
|
||||||
/homeassistant/components/cloudflare/ @ludeeus @ctalkington
|
/homeassistant/components/cloudflare/ @ludeeus @ctalkington
|
||||||
/tests/components/cloudflare/ @ludeeus @ctalkington
|
/tests/components/cloudflare/ @ludeeus @ctalkington
|
||||||
/homeassistant/components/co2signal/ @jpbede
|
/homeassistant/components/co2signal/ @jpbede @VIKTORVAV99
|
||||||
/tests/components/co2signal/ @jpbede
|
/tests/components/co2signal/ @jpbede @VIKTORVAV99
|
||||||
/homeassistant/components/coinbase/ @tombrien
|
/homeassistant/components/coinbase/ @tombrien
|
||||||
/tests/components/coinbase/ @tombrien
|
/tests/components/coinbase/ @tombrien
|
||||||
/homeassistant/components/color_extractor/ @GenericStudent
|
/homeassistant/components/color_extractor/ @GenericStudent
|
||||||
@ -295,6 +301,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/dormakaba_dkey/ @emontnemery
|
/tests/components/dormakaba_dkey/ @emontnemery
|
||||||
/homeassistant/components/dremel_3d_printer/ @tkdrob
|
/homeassistant/components/dremel_3d_printer/ @tkdrob
|
||||||
/tests/components/dremel_3d_printer/ @tkdrob
|
/tests/components/dremel_3d_printer/ @tkdrob
|
||||||
|
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||||
|
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||||
/homeassistant/components/dsmr/ @Robbie1221 @frenck
|
/homeassistant/components/dsmr/ @Robbie1221 @frenck
|
||||||
/tests/components/dsmr/ @Robbie1221 @frenck
|
/tests/components/dsmr/ @Robbie1221 @frenck
|
||||||
/homeassistant/components/dsmr_reader/ @depl0y @glodenox
|
/homeassistant/components/dsmr_reader/ @depl0y @glodenox
|
||||||
@ -344,7 +352,7 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/energy/ @home-assistant/core
|
/tests/components/energy/ @home-assistant/core
|
||||||
/homeassistant/components/energyzero/ @klaasnicolaas
|
/homeassistant/components/energyzero/ @klaasnicolaas
|
||||||
/tests/components/energyzero/ @klaasnicolaas
|
/tests/components/energyzero/ @klaasnicolaas
|
||||||
/homeassistant/components/enigma2/ @fbradyirl
|
/homeassistant/components/enigma2/ @autinerd
|
||||||
/homeassistant/components/enocean/ @bdurrer
|
/homeassistant/components/enocean/ @bdurrer
|
||||||
/tests/components/enocean/ @bdurrer
|
/tests/components/enocean/ @bdurrer
|
||||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
|
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
|
||||||
@ -395,6 +403,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/fivem/ @Sander0542
|
/tests/components/fivem/ @Sander0542
|
||||||
/homeassistant/components/fjaraskupan/ @elupus
|
/homeassistant/components/fjaraskupan/ @elupus
|
||||||
/tests/components/fjaraskupan/ @elupus
|
/tests/components/fjaraskupan/ @elupus
|
||||||
|
/homeassistant/components/flexit_bacnet/ @lellky @piotrbulinski
|
||||||
|
/tests/components/flexit_bacnet/ @lellky @piotrbulinski
|
||||||
/homeassistant/components/flick_electric/ @ZephireNZ
|
/homeassistant/components/flick_electric/ @ZephireNZ
|
||||||
/tests/components/flick_electric/ @ZephireNZ
|
/tests/components/flick_electric/ @ZephireNZ
|
||||||
/homeassistant/components/flipr/ @cnico
|
/homeassistant/components/flipr/ @cnico
|
||||||
@ -410,8 +420,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/forked_daapd/ @uvjustin
|
/homeassistant/components/forked_daapd/ @uvjustin
|
||||||
/tests/components/forked_daapd/ @uvjustin
|
/tests/components/forked_daapd/ @uvjustin
|
||||||
/homeassistant/components/fortios/ @kimfrellsen
|
/homeassistant/components/fortios/ @kimfrellsen
|
||||||
/homeassistant/components/foscam/ @skgsergio
|
/homeassistant/components/foscam/ @skgsergio @krmarien
|
||||||
/tests/components/foscam/ @skgsergio
|
/tests/components/foscam/ @skgsergio @krmarien
|
||||||
/homeassistant/components/freebox/ @hacf-fr @Quentame
|
/homeassistant/components/freebox/ @hacf-fr @Quentame
|
||||||
/tests/components/freebox/ @hacf-fr @Quentame
|
/tests/components/freebox/ @hacf-fr @Quentame
|
||||||
/homeassistant/components/freedompro/ @stefano055415
|
/homeassistant/components/freedompro/ @stefano055415
|
||||||
@ -520,6 +530,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/hive/ @Rendili @KJonline
|
/tests/components/hive/ @Rendili @KJonline
|
||||||
/homeassistant/components/hlk_sw16/ @jameshilliard
|
/homeassistant/components/hlk_sw16/ @jameshilliard
|
||||||
/tests/components/hlk_sw16/ @jameshilliard
|
/tests/components/hlk_sw16/ @jameshilliard
|
||||||
|
/homeassistant/components/holiday/ @jrieger
|
||||||
|
/tests/components/holiday/ @jrieger
|
||||||
/homeassistant/components/home_connect/ @DavidMStraub
|
/homeassistant/components/home_connect/ @DavidMStraub
|
||||||
/tests/components/home_connect/ @DavidMStraub
|
/tests/components/home_connect/ @DavidMStraub
|
||||||
/homeassistant/components/home_plus_control/ @chemaaa
|
/homeassistant/components/home_plus_control/ @chemaaa
|
||||||
@ -803,6 +815,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/motion_blinds/ @starkillerOG
|
/tests/components/motion_blinds/ @starkillerOG
|
||||||
/homeassistant/components/motioneye/ @dermotduffy
|
/homeassistant/components/motioneye/ @dermotduffy
|
||||||
/tests/components/motioneye/ @dermotduffy
|
/tests/components/motioneye/ @dermotduffy
|
||||||
|
/homeassistant/components/motionmount/ @RJPoelstra
|
||||||
|
/tests/components/motionmount/ @RJPoelstra
|
||||||
/homeassistant/components/mqtt/ @emontnemery @jbouwh
|
/homeassistant/components/mqtt/ @emontnemery @jbouwh
|
||||||
/tests/components/mqtt/ @emontnemery @jbouwh
|
/tests/components/mqtt/ @emontnemery @jbouwh
|
||||||
/homeassistant/components/msteams/ @peroyvind
|
/homeassistant/components/msteams/ @peroyvind
|
||||||
@ -833,6 +847,7 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
/homeassistant/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
||||||
/tests/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
/tests/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
||||||
/homeassistant/components/netgear_lte/ @tkdrob
|
/homeassistant/components/netgear_lte/ @tkdrob
|
||||||
|
/tests/components/netgear_lte/ @tkdrob
|
||||||
/homeassistant/components/network/ @home-assistant/core
|
/homeassistant/components/network/ @home-assistant/core
|
||||||
/tests/components/network/ @home-assistant/core
|
/tests/components/network/ @home-assistant/core
|
||||||
/homeassistant/components/nexia/ @bdraco
|
/homeassistant/components/nexia/ @bdraco
|
||||||
@ -926,6 +941,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/oralb/ @bdraco @Lash-L
|
/homeassistant/components/oralb/ @bdraco @Lash-L
|
||||||
/tests/components/oralb/ @bdraco @Lash-L
|
/tests/components/oralb/ @bdraco @Lash-L
|
||||||
/homeassistant/components/oru/ @bvlaicu
|
/homeassistant/components/oru/ @bvlaicu
|
||||||
|
/homeassistant/components/osoenergy/ @osohotwateriot
|
||||||
|
/tests/components/osoenergy/ @osohotwateriot
|
||||||
/homeassistant/components/otbr/ @home-assistant/core
|
/homeassistant/components/otbr/ @home-assistant/core
|
||||||
/tests/components/otbr/ @home-assistant/core
|
/tests/components/otbr/ @home-assistant/core
|
||||||
/homeassistant/components/ourgroceries/ @OnFreund
|
/homeassistant/components/ourgroceries/ @OnFreund
|
||||||
@ -985,8 +1002,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/proximity/ @mib1185
|
/homeassistant/components/proximity/ @mib1185
|
||||||
/tests/components/proximity/ @mib1185
|
/tests/components/proximity/ @mib1185
|
||||||
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
|
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
|
||||||
/homeassistant/components/prusalink/ @balloob
|
/homeassistant/components/prusalink/ @balloob @Skaronator
|
||||||
/tests/components/prusalink/ @balloob
|
/tests/components/prusalink/ @balloob @Skaronator
|
||||||
/homeassistant/components/ps4/ @ktnrg45
|
/homeassistant/components/ps4/ @ktnrg45
|
||||||
/tests/components/ps4/ @ktnrg45
|
/tests/components/ps4/ @ktnrg45
|
||||||
/homeassistant/components/pure_energie/ @klaasnicolaas
|
/homeassistant/components/pure_energie/ @klaasnicolaas
|
||||||
@ -1003,8 +1020,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/pvoutput/ @frenck
|
/tests/components/pvoutput/ @frenck
|
||||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue
|
/homeassistant/components/pvpc_hourly_pricing/ @azogue
|
||||||
/tests/components/pvpc_hourly_pricing/ @azogue
|
/tests/components/pvpc_hourly_pricing/ @azogue
|
||||||
/homeassistant/components/qbittorrent/ @geoffreylagaisse
|
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||||
/tests/components/qbittorrent/ @geoffreylagaisse
|
/tests/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||||
/homeassistant/components/qingping/ @bdraco @skgsergio
|
/homeassistant/components/qingping/ @bdraco @skgsergio
|
||||||
/tests/components/qingping/ @bdraco @skgsergio
|
/tests/components/qingping/ @bdraco @skgsergio
|
||||||
/homeassistant/components/qld_bushfire/ @exxamalte
|
/homeassistant/components/qld_bushfire/ @exxamalte
|
||||||
@ -1046,6 +1063,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/recorder/ @home-assistant/core
|
/tests/components/recorder/ @home-assistant/core
|
||||||
/homeassistant/components/recovery_mode/ @home-assistant/core
|
/homeassistant/components/recovery_mode/ @home-assistant/core
|
||||||
/tests/components/recovery_mode/ @home-assistant/core
|
/tests/components/recovery_mode/ @home-assistant/core
|
||||||
|
/homeassistant/components/refoss/ @ashionky
|
||||||
|
/tests/components/refoss/ @ashionky
|
||||||
/homeassistant/components/rejseplanen/ @DarkFox
|
/homeassistant/components/rejseplanen/ @DarkFox
|
||||||
/homeassistant/components/remote/ @home-assistant/core
|
/homeassistant/components/remote/ @home-assistant/core
|
||||||
/tests/components/remote/ @home-assistant/core
|
/tests/components/remote/ @home-assistant/core
|
||||||
@ -1058,6 +1077,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/repairs/ @home-assistant/core
|
/homeassistant/components/repairs/ @home-assistant/core
|
||||||
/tests/components/repairs/ @home-assistant/core
|
/tests/components/repairs/ @home-assistant/core
|
||||||
/homeassistant/components/repetier/ @ShadowBr0ther
|
/homeassistant/components/repetier/ @ShadowBr0ther
|
||||||
|
/homeassistant/components/rest_command/ @jpbede
|
||||||
|
/tests/components/rest_command/ @jpbede
|
||||||
/homeassistant/components/rflink/ @javicalle
|
/homeassistant/components/rflink/ @javicalle
|
||||||
/tests/components/rflink/ @javicalle
|
/tests/components/rflink/ @javicalle
|
||||||
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
||||||
@ -1243,13 +1264,17 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/subaru/ @G-Two
|
/homeassistant/components/subaru/ @G-Two
|
||||||
/tests/components/subaru/ @G-Two
|
/tests/components/subaru/ @G-Two
|
||||||
/homeassistant/components/suez_water/ @ooii
|
/homeassistant/components/suez_water/ @ooii
|
||||||
|
/tests/components/suez_water/ @ooii
|
||||||
/homeassistant/components/sun/ @Swamp-Ig
|
/homeassistant/components/sun/ @Swamp-Ig
|
||||||
/tests/components/sun/ @Swamp-Ig
|
/tests/components/sun/ @Swamp-Ig
|
||||||
|
/homeassistant/components/sunweg/ @rokam
|
||||||
|
/tests/components/sunweg/ @rokam
|
||||||
/homeassistant/components/supla/ @mwegrzynek
|
/homeassistant/components/supla/ @mwegrzynek
|
||||||
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
||||||
/tests/components/surepetcare/ @benleb @danielhiversen
|
/tests/components/surepetcare/ @benleb @danielhiversen
|
||||||
/homeassistant/components/swiss_hydrological_data/ @fabaff
|
/homeassistant/components/swiss_hydrological_data/ @fabaff
|
||||||
/homeassistant/components/swiss_public_transport/ @fabaff
|
/homeassistant/components/swiss_public_transport/ @fabaff @miaucl
|
||||||
|
/tests/components/swiss_public_transport/ @fabaff @miaucl
|
||||||
/homeassistant/components/switch/ @home-assistant/core
|
/homeassistant/components/switch/ @home-assistant/core
|
||||||
/tests/components/switch/ @home-assistant/core
|
/tests/components/switch/ @home-assistant/core
|
||||||
/homeassistant/components/switch_as_x/ @home-assistant/core
|
/homeassistant/components/switch_as_x/ @home-assistant/core
|
||||||
@ -1272,12 +1297,16 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/synology_srm/ @aerialls
|
/homeassistant/components/synology_srm/ @aerialls
|
||||||
/homeassistant/components/system_bridge/ @timmo001
|
/homeassistant/components/system_bridge/ @timmo001
|
||||||
/tests/components/system_bridge/ @timmo001
|
/tests/components/system_bridge/ @timmo001
|
||||||
/homeassistant/components/tado/ @michaelarnauts @chiefdragon
|
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
||||||
/tests/components/tado/ @michaelarnauts @chiefdragon
|
/tests/components/systemmonitor/ @gjohansson-ST
|
||||||
|
/homeassistant/components/tado/ @michaelarnauts @chiefdragon @erwindouna
|
||||||
|
/tests/components/tado/ @michaelarnauts @chiefdragon @erwindouna
|
||||||
/homeassistant/components/tag/ @balloob @dmulcahey
|
/homeassistant/components/tag/ @balloob @dmulcahey
|
||||||
/tests/components/tag/ @balloob @dmulcahey
|
/tests/components/tag/ @balloob @dmulcahey
|
||||||
/homeassistant/components/tailscale/ @frenck
|
/homeassistant/components/tailscale/ @frenck
|
||||||
/tests/components/tailscale/ @frenck
|
/tests/components/tailscale/ @frenck
|
||||||
|
/homeassistant/components/tailwind/ @frenck
|
||||||
|
/tests/components/tailwind/ @frenck
|
||||||
/homeassistant/components/tami4/ @Guy293
|
/homeassistant/components/tami4/ @Guy293
|
||||||
/tests/components/tami4/ @Guy293
|
/tests/components/tami4/ @Guy293
|
||||||
/homeassistant/components/tankerkoenig/ @guillempages @mib1185
|
/homeassistant/components/tankerkoenig/ @guillempages @mib1185
|
||||||
@ -1293,6 +1322,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
||||||
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
||||||
/tests/components/tesla_wall_connector/ @einarhauks
|
/tests/components/tesla_wall_connector/ @einarhauks
|
||||||
|
/homeassistant/components/tessie/ @Bre77
|
||||||
|
/tests/components/tessie/ @Bre77
|
||||||
/homeassistant/components/text/ @home-assistant/core
|
/homeassistant/components/text/ @home-assistant/core
|
||||||
/tests/components/text/ @home-assistant/core
|
/tests/components/text/ @home-assistant/core
|
||||||
/homeassistant/components/tfiac/ @fredrike @mellado
|
/homeassistant/components/tfiac/ @fredrike @mellado
|
||||||
@ -1360,6 +1391,7 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ukraine_alarm/ @PaulAnnekov
|
/tests/components/ukraine_alarm/ @PaulAnnekov
|
||||||
/homeassistant/components/unifi/ @Kane610
|
/homeassistant/components/unifi/ @Kane610
|
||||||
/tests/components/unifi/ @Kane610
|
/tests/components/unifi/ @Kane610
|
||||||
|
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
||||||
/homeassistant/components/unifiled/ @florisvdk
|
/homeassistant/components/unifiled/ @florisvdk
|
||||||
/homeassistant/components/unifiprotect/ @AngellusMortis @bdraco
|
/homeassistant/components/unifiprotect/ @AngellusMortis @bdraco
|
||||||
/tests/components/unifiprotect/ @AngellusMortis @bdraco
|
/tests/components/unifiprotect/ @AngellusMortis @bdraco
|
||||||
@ -1388,6 +1420,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/vacuum/ @home-assistant/core
|
/tests/components/vacuum/ @home-assistant/core
|
||||||
/homeassistant/components/vallox/ @andre-richter @slovdahl @viiru-
|
/homeassistant/components/vallox/ @andre-richter @slovdahl @viiru-
|
||||||
/tests/components/vallox/ @andre-richter @slovdahl @viiru-
|
/tests/components/vallox/ @andre-richter @slovdahl @viiru-
|
||||||
|
/homeassistant/components/valve/ @home-assistant/core
|
||||||
|
/tests/components/valve/ @home-assistant/core
|
||||||
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
||||||
/tests/components/velbus/ @Cereal2nd @brefra
|
/tests/components/velbus/ @Cereal2nd @brefra
|
||||||
/homeassistant/components/velux/ @Julius2342
|
/homeassistant/components/velux/ @Julius2342
|
||||||
@ -1396,8 +1430,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/versasense/ @imstevenxyz
|
/homeassistant/components/versasense/ @imstevenxyz
|
||||||
/homeassistant/components/version/ @ludeeus
|
/homeassistant/components/version/ @ludeeus
|
||||||
/tests/components/version/ @ludeeus
|
/tests/components/version/ @ludeeus
|
||||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey
|
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
|
||||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey
|
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
|
||||||
/homeassistant/components/vicare/ @CFenner
|
/homeassistant/components/vicare/ @CFenner
|
||||||
/tests/components/vicare/ @CFenner
|
/tests/components/vicare/ @CFenner
|
||||||
/homeassistant/components/vilfo/ @ManneW
|
/homeassistant/components/vilfo/ @ManneW
|
||||||
|
@ -6,7 +6,7 @@ FROM ${BUILD_FROM}
|
|||||||
|
|
||||||
# Synchronize with homeassistant/core.py:async_stop
|
# Synchronize with homeassistant/core.py:async_stop
|
||||||
ENV \
|
ENV \
|
||||||
S6_SERVICES_GRACETIME=220000
|
S6_SERVICES_GRACETIME=240000
|
||||||
|
|
||||||
ARG QEMU_CPU
|
ARG QEMU_CPU
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ from .const import (
|
|||||||
from .exceptions import HomeAssistantError
|
from .exceptions import HomeAssistantError
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
area_registry,
|
area_registry,
|
||||||
|
config_validation as cv,
|
||||||
device_registry,
|
device_registry,
|
||||||
entity,
|
entity,
|
||||||
entity_registry,
|
entity_registry,
|
||||||
@ -473,7 +474,9 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
|
|||||||
def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
|
def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
|
||||||
"""Get domains of components to set up."""
|
"""Get domains of components to set up."""
|
||||||
# Filter out the repeating and common config section [homeassistant]
|
# Filter out the repeating and common config section [homeassistant]
|
||||||
domains = {key.partition(" ")[0] for key in config if key != core.DOMAIN}
|
domains = {
|
||||||
|
domain for key in config if (domain := cv.domain_key(key)) != core.DOMAIN
|
||||||
|
}
|
||||||
|
|
||||||
# Add config entry domains
|
# Add config entry domains
|
||||||
if not hass.config.recovery_mode:
|
if not hass.config.recovery_mode:
|
||||||
|
5
homeassistant/brands/flexit.json
Normal file
5
homeassistant/brands/flexit.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "flexit",
|
||||||
|
"name": "Flexit",
|
||||||
|
"integrations": ["flexit", "flexit_bacnet"]
|
||||||
|
}
|
@ -27,7 +27,7 @@ ABODE_TEMPERATURE_UNIT_HA_UNIT = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AbodeSensorDescriptionMixin:
|
class AbodeSensorDescriptionMixin:
|
||||||
"""Mixin for Abode sensor."""
|
"""Mixin for Abode sensor."""
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ class AbodeSensorDescriptionMixin:
|
|||||||
native_unit_of_measurement_fn: Callable[[AbodeSense], str]
|
native_unit_of_measurement_fn: Callable[[AbodeSense], str]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AbodeSensorDescription(SensorEntityDescription, AbodeSensorDescriptionMixin):
|
class AbodeSensorDescription(SensorEntityDescription, AbodeSensorDescriptionMixin):
|
||||||
"""Class describing Abode sensor entities."""
|
"""Class describing Abode sensor entities."""
|
||||||
|
|
||||||
|
@ -45,14 +45,14 @@ from .const import (
|
|||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AccuWeatherSensorDescriptionMixin:
|
class AccuWeatherSensorDescriptionMixin:
|
||||||
"""Mixin for AccuWeather sensor."""
|
"""Mixin for AccuWeather sensor."""
|
||||||
|
|
||||||
value_fn: Callable[[dict[str, Any]], str | int | float | None]
|
value_fn: Callable[[dict[str, Any]], str | int | float | None]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AccuWeatherSensorDescription(
|
class AccuWeatherSensorDescription(
|
||||||
SensorEntityDescription, AccuWeatherSensorDescriptionMixin
|
SensorEntityDescription, AccuWeatherSensorDescriptionMixin
|
||||||
):
|
):
|
||||||
|
@ -24,7 +24,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
# convert title and unique_id to string
|
# convert title and unique_id to string
|
||||||
if config_entry.version == 1:
|
if config_entry.version == 1:
|
||||||
if isinstance(config_entry.unique_id, int):
|
if isinstance(config_entry.unique_id, int):
|
||||||
hass.config_entries.async_update_entry(
|
hass.config_entries.async_update_entry( # type: ignore[unreachable]
|
||||||
config_entry,
|
config_entry,
|
||||||
unique_id=str(config_entry.unique_id),
|
unique_id=str(config_entry.unique_id),
|
||||||
title=str(config_entry.title),
|
title=str(config_entry.title),
|
||||||
|
@ -137,7 +137,7 @@ class LocalAdaxDevice(ClimateEntity):
|
|||||||
_attr_target_temperature_step = PRECISION_WHOLE
|
_attr_target_temperature_step = PRECISION_WHOLE
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
|
|
||||||
def __init__(self, adax_data_handler, unique_id):
|
def __init__(self, adax_data_handler: AdaxLocal, unique_id: str) -> None:
|
||||||
"""Initialize the heater."""
|
"""Initialize the heater."""
|
||||||
self._adax_data_handler = adax_data_handler
|
self._adax_data_handler = adax_data_handler
|
||||||
self._attr_unique_id = unique_id
|
self._attr_unique_id = unique_id
|
||||||
|
@ -36,7 +36,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
VERSION = 2
|
VERSION = 2
|
||||||
|
|
||||||
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."""
|
"""Handle the initial step."""
|
||||||
data_schema = vol.Schema(
|
data_schema = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -59,7 +61,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
return await self.async_step_local()
|
return await self.async_step_local()
|
||||||
return await self.async_step_cloud()
|
return await self.async_step_cloud()
|
||||||
|
|
||||||
async def async_step_local(self, user_input=None):
|
async def async_step_local(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
"""Handle the local step."""
|
"""Handle the local step."""
|
||||||
data_schema = vol.Schema(
|
data_schema = vol.Schema(
|
||||||
{vol.Required(WIFI_SSID): str, vol.Required(WIFI_PSWD): str}
|
{vol.Required(WIFI_SSID): str, vol.Required(WIFI_PSWD): str}
|
||||||
|
@ -22,7 +22,7 @@ SCAN_INTERVAL = timedelta(seconds=300)
|
|||||||
PARALLEL_UPDATES = 4
|
PARALLEL_UPDATES = 4
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AdGuardHomeEntityDescription(SensorEntityDescription):
|
class AdGuardHomeEntityDescription(SensorEntityDescription):
|
||||||
"""Describes AdGuard Home sensor entity."""
|
"""Describes AdGuard Home sensor entity."""
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ SCAN_INTERVAL = timedelta(seconds=10)
|
|||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AdGuardHomeSwitchEntityDescription(SwitchEntityDescription):
|
class AdGuardHomeSwitchEntityDescription(SwitchEntityDescription):
|
||||||
"""Describes AdGuard Home switch entity."""
|
"""Describes AdGuard Home switch entity."""
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform
|
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import ADVANTAGE_AIR_RETRY, DOMAIN
|
from .const import ADVANTAGE_AIR_RETRY, DOMAIN
|
||||||
@ -26,6 +27,7 @@ PLATFORMS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
REQUEST_REFRESH_DELAY = 0.5
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
@ -51,6 +53,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
name="Advantage Air",
|
name="Advantage Air",
|
||||||
update_method=async_get,
|
update_method=async_get,
|
||||||
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
|
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
|
||||||
|
request_refresh_debouncer=Debouncer(
|
||||||
|
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
||||||
ADVANTAGE_AIR_STATE_CLOSE,
|
ADVANTAGE_AIR_STATE_CLOSE,
|
||||||
ADVANTAGE_AIR_STATE_OFF,
|
ADVANTAGE_AIR_STATE_OFF,
|
||||||
ADVANTAGE_AIR_STATE_ON,
|
ADVANTAGE_AIR_STATE_ON,
|
||||||
@ -39,16 +40,6 @@ ADVANTAGE_AIR_HVAC_MODES = {
|
|||||||
}
|
}
|
||||||
HASS_HVAC_MODES = {v: k for k, v in ADVANTAGE_AIR_HVAC_MODES.items()}
|
HASS_HVAC_MODES = {v: k for k, v in ADVANTAGE_AIR_HVAC_MODES.items()}
|
||||||
|
|
||||||
ADVANTAGE_AIR_FAN_MODES = {
|
|
||||||
"autoAA": FAN_AUTO,
|
|
||||||
"low": FAN_LOW,
|
|
||||||
"medium": FAN_MEDIUM,
|
|
||||||
"high": FAN_HIGH,
|
|
||||||
}
|
|
||||||
HASS_FAN_MODES = {v: k for k, v in ADVANTAGE_AIR_FAN_MODES.items()}
|
|
||||||
FAN_SPEEDS = {FAN_LOW: 30, FAN_MEDIUM: 60, FAN_HIGH: 100}
|
|
||||||
|
|
||||||
ADVANTAGE_AIR_AUTOFAN = "aaAutoFanModeEnabled"
|
|
||||||
ADVANTAGE_AIR_MYZONE = "MyZone"
|
ADVANTAGE_AIR_MYZONE = "MyZone"
|
||||||
ADVANTAGE_AIR_MYAUTO = "MyAuto"
|
ADVANTAGE_AIR_MYAUTO = "MyAuto"
|
||||||
ADVANTAGE_AIR_MYAUTO_ENABLED = "myAutoModeEnabled"
|
ADVANTAGE_AIR_MYAUTO_ENABLED = "myAutoModeEnabled"
|
||||||
@ -56,6 +47,7 @@ ADVANTAGE_AIR_MYTEMP = "MyTemp"
|
|||||||
ADVANTAGE_AIR_MYTEMP_ENABLED = "climateControlModeEnabled"
|
ADVANTAGE_AIR_MYTEMP_ENABLED = "climateControlModeEnabled"
|
||||||
ADVANTAGE_AIR_HEAT_TARGET = "myAutoHeatTargetTemp"
|
ADVANTAGE_AIR_HEAT_TARGET = "myAutoHeatTargetTemp"
|
||||||
ADVANTAGE_AIR_COOL_TARGET = "myAutoCoolTargetTemp"
|
ADVANTAGE_AIR_COOL_TARGET = "myAutoCoolTargetTemp"
|
||||||
|
ADVANTAGE_AIR_MYFAN = "autoAA"
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
@ -85,27 +77,25 @@ async def async_setup_entry(
|
|||||||
class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
||||||
"""AdvantageAir AC unit."""
|
"""AdvantageAir AC unit."""
|
||||||
|
|
||||||
_attr_fan_modes = [FAN_LOW, FAN_MEDIUM, FAN_HIGH]
|
_attr_fan_modes = [FAN_LOW, FAN_MEDIUM, FAN_HIGH, FAN_AUTO]
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
_attr_target_temperature_step = PRECISION_WHOLE
|
_attr_target_temperature_step = PRECISION_WHOLE
|
||||||
_attr_max_temp = 32
|
_attr_max_temp = 32
|
||||||
_attr_min_temp = 16
|
_attr_min_temp = 16
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
|
|
||||||
_attr_hvac_modes = [
|
|
||||||
HVACMode.OFF,
|
|
||||||
HVACMode.COOL,
|
|
||||||
HVACMode.HEAT,
|
|
||||||
HVACMode.FAN_ONLY,
|
|
||||||
HVACMode.DRY,
|
|
||||||
]
|
|
||||||
|
|
||||||
_attr_supported_features = ClimateEntityFeature.FAN_MODE
|
|
||||||
|
|
||||||
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
|
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
|
||||||
"""Initialize an AdvantageAir AC unit."""
|
"""Initialize an AdvantageAir AC unit."""
|
||||||
super().__init__(instance, ac_key)
|
super().__init__(instance, ac_key)
|
||||||
|
|
||||||
|
self._attr_supported_features = ClimateEntityFeature.FAN_MODE
|
||||||
|
self._attr_hvac_modes = [
|
||||||
|
HVACMode.OFF,
|
||||||
|
HVACMode.COOL,
|
||||||
|
HVACMode.HEAT,
|
||||||
|
HVACMode.FAN_ONLY,
|
||||||
|
HVACMode.DRY,
|
||||||
|
]
|
||||||
# Set supported features and HVAC modes based on current operating mode
|
# Set supported features and HVAC modes based on current operating mode
|
||||||
if self._ac.get(ADVANTAGE_AIR_MYAUTO_ENABLED):
|
if self._ac.get(ADVANTAGE_AIR_MYAUTO_ENABLED):
|
||||||
# MyAuto
|
# MyAuto
|
||||||
@ -118,10 +108,6 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
|||||||
# MyZone
|
# MyZone
|
||||||
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
|
||||||
# Add "ezfan" mode if supported
|
|
||||||
if self._ac.get(ADVANTAGE_AIR_AUTOFAN):
|
|
||||||
self._attr_fan_modes += [FAN_AUTO]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self) -> float | None:
|
def current_temperature(self) -> float | None:
|
||||||
"""Return the selected zones current temperature."""
|
"""Return the selected zones current temperature."""
|
||||||
@ -151,7 +137,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
|||||||
@property
|
@property
|
||||||
def fan_mode(self) -> str | None:
|
def fan_mode(self) -> str | None:
|
||||||
"""Return the current fan modes."""
|
"""Return the current fan modes."""
|
||||||
return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"])
|
return FAN_AUTO if self._ac["fan"] == ADVANTAGE_AIR_MYFAN else self._ac["fan"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature_high(self) -> float | None:
|
def target_temperature_high(self) -> float | None:
|
||||||
@ -189,7 +175,11 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
|||||||
|
|
||||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||||
"""Set the Fan Mode."""
|
"""Set the Fan Mode."""
|
||||||
await self.async_update_ac({"fan": HASS_FAN_MODES.get(fan_mode)})
|
if fan_mode == FAN_AUTO and self._ac.get(ADVANTAGE_AIR_AUTOFAN_ENABLED):
|
||||||
|
mode = ADVANTAGE_AIR_MYFAN
|
||||||
|
else:
|
||||||
|
mode = fan_mode
|
||||||
|
await self.async_update_ac({"fan": mode})
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set the Temperature."""
|
"""Set the Temperature."""
|
||||||
|
@ -5,3 +5,4 @@ ADVANTAGE_AIR_STATE_OPEN = "open"
|
|||||||
ADVANTAGE_AIR_STATE_CLOSE = "close"
|
ADVANTAGE_AIR_STATE_CLOSE = "close"
|
||||||
ADVANTAGE_AIR_STATE_ON = "on"
|
ADVANTAGE_AIR_STATE_ON = "on"
|
||||||
ADVANTAGE_AIR_STATE_OFF = "off"
|
ADVANTAGE_AIR_STATE_OFF = "off"
|
||||||
|
ADVANTAGE_AIR_AUTOFAN_ENABLED = "aaAutoFanModeEnabled"
|
||||||
|
@ -30,7 +30,7 @@ class AdvantageAirEntity(CoordinatorEntity):
|
|||||||
async def update_handle(*values):
|
async def update_handle(*values):
|
||||||
try:
|
try:
|
||||||
if await func(*keys, *values):
|
if await func(*keys, *values):
|
||||||
await self.coordinator.async_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
except ApiError as err:
|
except ApiError as err:
|
||||||
raise HomeAssistantError(err) from err
|
raise HomeAssistantError(err) from err
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
||||||
ADVANTAGE_AIR_STATE_OFF,
|
ADVANTAGE_AIR_STATE_OFF,
|
||||||
ADVANTAGE_AIR_STATE_ON,
|
ADVANTAGE_AIR_STATE_ON,
|
||||||
DOMAIN as ADVANTAGE_AIR_DOMAIN,
|
DOMAIN as ADVANTAGE_AIR_DOMAIN,
|
||||||
@ -29,6 +30,8 @@ async def async_setup_entry(
|
|||||||
for ac_key, ac_device in aircons.items():
|
for ac_key, ac_device in aircons.items():
|
||||||
if ac_device["info"]["freshAirStatus"] != "none":
|
if ac_device["info"]["freshAirStatus"] != "none":
|
||||||
entities.append(AdvantageAirFreshAir(instance, ac_key))
|
entities.append(AdvantageAirFreshAir(instance, ac_key))
|
||||||
|
if ADVANTAGE_AIR_AUTOFAN_ENABLED in ac_device["info"]:
|
||||||
|
entities.append(AdvantageAirMyFan(instance, ac_key))
|
||||||
if things := instance.coordinator.data.get("myThings"):
|
if things := instance.coordinator.data.get("myThings"):
|
||||||
for thing in things["things"].values():
|
for thing in things["things"].values():
|
||||||
if thing["channelDipState"] == 8: # 8 = Other relay
|
if thing["channelDipState"] == 8: # 8 = Other relay
|
||||||
@ -62,6 +65,32 @@ class AdvantageAirFreshAir(AdvantageAirAcEntity, SwitchEntity):
|
|||||||
await self.async_update_ac({"freshAirStatus": ADVANTAGE_AIR_STATE_OFF})
|
await self.async_update_ac({"freshAirStatus": ADVANTAGE_AIR_STATE_OFF})
|
||||||
|
|
||||||
|
|
||||||
|
class AdvantageAirMyFan(AdvantageAirAcEntity, SwitchEntity):
|
||||||
|
"""Representation of Advantage Air MyFan control."""
|
||||||
|
|
||||||
|
_attr_icon = "mdi:fan-auto"
|
||||||
|
_attr_name = "MyFan"
|
||||||
|
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||||
|
|
||||||
|
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
|
||||||
|
"""Initialize an Advantage Air MyFan control."""
|
||||||
|
super().__init__(instance, ac_key)
|
||||||
|
self._attr_unique_id += "-myfan"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return the MyFan status."""
|
||||||
|
return self._ac[ADVANTAGE_AIR_AUTOFAN_ENABLED]
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn MyFan on."""
|
||||||
|
await self.async_update_ac({ADVANTAGE_AIR_AUTOFAN_ENABLED: True})
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn MyFan off."""
|
||||||
|
await self.async_update_ac({ADVANTAGE_AIR_AUTOFAN_ENABLED: False})
|
||||||
|
|
||||||
|
|
||||||
class AdvantageAirRelay(AdvantageAirThingEntity, SwitchEntity):
|
class AdvantageAirRelay(AdvantageAirThingEntity, SwitchEntity):
|
||||||
"""Representation of Advantage Air Thing."""
|
"""Representation of Advantage Air Thing."""
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""Config flow for AEMET OpenData."""
|
"""Config flow for AEMET OpenData."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from aemet_opendata.exceptions import AuthError
|
from aemet_opendata.exceptions import AuthError
|
||||||
from aemet_opendata.interface import AEMET, ConnectionOptions
|
from aemet_opendata.interface import AEMET, ConnectionOptions
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -8,6 +10,7 @@ import voluptuous as vol
|
|||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||||
from homeassistant.helpers.schema_config_entry_flow import (
|
from homeassistant.helpers.schema_config_entry_flow import (
|
||||||
SchemaFlowFormStep,
|
SchemaFlowFormStep,
|
||||||
@ -29,7 +32,9 @@ OPTIONS_FLOW = {
|
|||||||
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Config flow for AEMET OpenData."""
|
"""Config flow for AEMET OpenData."""
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
"""Handle a flow initialized by the user."""
|
"""Handle a flow initialized by the user."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
|
1
homeassistant/components/aep_ohio/__init__.py
Normal file
1
homeassistant/components/aep_ohio/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Virtual integration: AEP Ohio."""
|
6
homeassistant/components/aep_ohio/manifest.json
Normal file
6
homeassistant/components/aep_ohio/manifest.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"domain": "aep_ohio",
|
||||||
|
"name": "AEP Ohio",
|
||||||
|
"integration_type": "virtual",
|
||||||
|
"supported_by": "opower"
|
||||||
|
}
|
1
homeassistant/components/aep_texas/__init__.py
Normal file
1
homeassistant/components/aep_texas/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Virtual integration: AEP Texas."""
|
6
homeassistant/components/aep_texas/manifest.json
Normal file
6
homeassistant/components/aep_texas/manifest.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"domain": "aep_texas",
|
||||||
|
"name": "AEP Texas",
|
||||||
|
"integration_type": "virtual",
|
||||||
|
"supported_by": "opower"
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
"""Config flow to configure Agent devices."""
|
"""Config flow to configure Agent devices."""
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from agent import AgentConnectionError, AgentError
|
from agent import AgentConnectionError, AgentError
|
||||||
from agent.a import Agent
|
from agent.a import Agent
|
||||||
@ -7,6 +8,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DOMAIN, SERVER_URL
|
from .const import DOMAIN, SERVER_URL
|
||||||
@ -18,11 +20,9 @@ DEFAULT_PORT = 8090
|
|||||||
class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle an Agent config flow."""
|
"""Handle an Agent config flow."""
|
||||||
|
|
||||||
def __init__(self):
|
async def async_step_user(
|
||||||
"""Initialize the Agent config flow."""
|
self, user_input: dict[str, Any] | None = None
|
||||||
self.device_config = {}
|
) -> FlowResult:
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
|
||||||
"""Handle an Agent config flow."""
|
"""Handle an Agent config flow."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
@ -49,13 +49,15 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.device_config = {
|
device_config = {
|
||||||
CONF_HOST: host,
|
CONF_HOST: host,
|
||||||
CONF_PORT: port,
|
CONF_PORT: port,
|
||||||
SERVER_URL: server_origin,
|
SERVER_URL: server_origin,
|
||||||
}
|
}
|
||||||
|
|
||||||
return await self._create_entry(agent_client.name)
|
return self.async_create_entry(
|
||||||
|
title=agent_client.name, data=device_config
|
||||||
|
)
|
||||||
|
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
|
|
||||||
@ -66,11 +68,6 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
description_placeholders=self.device_config,
|
|
||||||
data_schema=vol.Schema(data),
|
data_schema=vol.Schema(data),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _create_entry(self, server_name):
|
|
||||||
"""Create entry for device."""
|
|
||||||
return self.async_create_entry(title=server_name, data=self.device_config)
|
|
||||||
|
@ -56,7 +56,7 @@ from .const import (
|
|||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AirlySensorEntityDescription(SensorEntityDescription):
|
class AirlySensorEntityDescription(SensorEntityDescription):
|
||||||
"""Class describing Airly sensor entities."""
|
"""Class describing Airly sensor entities."""
|
||||||
|
|
||||||
|
@ -6,8 +6,17 @@ from pyairnow import WebServiceAPI
|
|||||||
from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError
|
from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries, core, data_entry_flow, exceptions
|
from homeassistant import core
|
||||||
|
from homeassistant.config_entries import (
|
||||||
|
ConfigEntry,
|
||||||
|
ConfigFlow,
|
||||||
|
OptionsFlow,
|
||||||
|
OptionsFlowWithConfigEntry,
|
||||||
|
)
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
@ -16,7 +25,7 @@ from .const import DOMAIN
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def validate_input(hass: core.HomeAssistant, data):
|
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
|
||||||
"""Validate the user input allows us to connect.
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||||
@ -46,12 +55,14 @@ async def validate_input(hass: core.HomeAssistant, data):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for AirNow."""
|
"""Handle a config flow for AirNow."""
|
||||||
|
|
||||||
VERSION = 2
|
VERSION = 2
|
||||||
|
|
||||||
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."""
|
"""Handle the initial step."""
|
||||||
errors = {}
|
errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
@ -108,18 +119,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@core.callback
|
@core.callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
config_entry: config_entries.ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
) -> config_entries.OptionsFlow:
|
) -> OptionsFlow:
|
||||||
"""Return the options flow."""
|
"""Return the options flow."""
|
||||||
return OptionsFlowHandler(config_entry)
|
return AirNowOptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
class OptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):
|
class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||||
"""Handle an options flow for AirNow."""
|
"""Handle an options flow for AirNow."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> data_entry_flow.FlowResult:
|
) -> FlowResult:
|
||||||
"""Manage the options."""
|
"""Manage the options."""
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return self.async_create_entry(data=user_input)
|
return self.async_create_entry(data=user_input)
|
||||||
@ -141,13 +152,13 @@ class OptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CannotConnect(exceptions.HomeAssistantError):
|
class CannotConnect(HomeAssistantError):
|
||||||
"""Error to indicate we cannot connect."""
|
"""Error to indicate we cannot connect."""
|
||||||
|
|
||||||
|
|
||||||
class InvalidAuth(exceptions.HomeAssistantError):
|
class InvalidAuth(HomeAssistantError):
|
||||||
"""Error to indicate there is invalid auth."""
|
"""Error to indicate there is invalid auth."""
|
||||||
|
|
||||||
|
|
||||||
class InvalidLocation(exceptions.HomeAssistantError):
|
class InvalidLocation(HomeAssistantError):
|
||||||
"""Error to indicate the location is invalid."""
|
"""Error to indicate the location is invalid."""
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
"""DataUpdateCoordinator for the AirNow integration."""
|
"""DataUpdateCoordinator for the AirNow integration."""
|
||||||
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiohttp import ClientSession
|
||||||
from aiohttp.client_exceptions import ClientConnectorError
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
from pyairnow import WebServiceAPI
|
from pyairnow import WebServiceAPI
|
||||||
from pyairnow.conv import aqi_to_concentration
|
from pyairnow.conv import aqi_to_concentration
|
||||||
from pyairnow.errors import AirNowError
|
from pyairnow.errors import AirNowError
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -31,12 +35,19 @@ from .const import (
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AirNowDataUpdateCoordinator(DataUpdateCoordinator):
|
class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
"""The AirNow update coordinator."""
|
"""The AirNow update coordinator."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hass, session, api_key, latitude, longitude, distance, update_interval
|
self,
|
||||||
):
|
hass: HomeAssistant,
|
||||||
|
session: ClientSession,
|
||||||
|
api_key: str,
|
||||||
|
latitude: float,
|
||||||
|
longitude: float,
|
||||||
|
distance: int,
|
||||||
|
update_interval: timedelta,
|
||||||
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self.latitude = latitude
|
self.latitude = latitude
|
||||||
self.longitude = longitude
|
self.longitude = longitude
|
||||||
@ -46,7 +57,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
|
|
||||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
|
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
|
||||||
|
|
||||||
async def _async_update_data(self):
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
"""Update data via library."""
|
"""Update data via library."""
|
||||||
data = {}
|
data = {}
|
||||||
try:
|
try:
|
||||||
|
@ -51,7 +51,7 @@ ATTR_LEVEL = "level"
|
|||||||
ATTR_STATION = "reporting_station"
|
ATTR_STATION = "reporting_station"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AirNowEntityDescriptionMixin:
|
class AirNowEntityDescriptionMixin:
|
||||||
"""Mixin for required keys."""
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ class AirNowEntityDescriptionMixin:
|
|||||||
extra_state_attributes_fn: Callable[[Any], dict[str, str]] | None
|
extra_state_attributes_fn: Callable[[Any], dict[str, str]] | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMixin):
|
class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMixin):
|
||||||
"""Describes Airnow sensor entity."""
|
"""Describes Airnow sensor entity."""
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aioairq import AirQ, InvalidAuth, InvalidInput
|
from aioairq import AirQ, InvalidAuth
|
||||||
from aiohttp.client_exceptions import ClientConnectionError
|
from aiohttp.client_exceptions import ClientConnectionError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -42,44 +42,32 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
session = async_get_clientsession(self.hass)
|
session = async_get_clientsession(self.hass)
|
||||||
|
airq = AirQ(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], session)
|
||||||
try:
|
try:
|
||||||
airq = AirQ(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], session)
|
await airq.validate()
|
||||||
except InvalidInput:
|
except ClientConnectionError:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s does not appear to be a valid IP address or mDNS name",
|
(
|
||||||
|
"Failed to connect to device %s. Check the IP address / device"
|
||||||
|
" ID as well as whether the device is connected to power and"
|
||||||
|
" the WiFi"
|
||||||
|
),
|
||||||
user_input[CONF_IP_ADDRESS],
|
user_input[CONF_IP_ADDRESS],
|
||||||
)
|
)
|
||||||
errors["base"] = "invalid_input"
|
errors["base"] = "cannot_connect"
|
||||||
|
except InvalidAuth:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Incorrect password for device %s", user_input[CONF_IP_ADDRESS]
|
||||||
|
)
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
else:
|
else:
|
||||||
try:
|
_LOGGER.debug("Successfully connected to %s", user_input[CONF_IP_ADDRESS])
|
||||||
await airq.validate()
|
|
||||||
except ClientConnectionError:
|
|
||||||
_LOGGER.debug(
|
|
||||||
(
|
|
||||||
"Failed to connect to device %s. Check the IP address / device"
|
|
||||||
" ID as well as whether the device is connected to power and"
|
|
||||||
" the WiFi"
|
|
||||||
),
|
|
||||||
user_input[CONF_IP_ADDRESS],
|
|
||||||
)
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
except InvalidAuth:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Incorrect password for device %s", user_input[CONF_IP_ADDRESS]
|
|
||||||
)
|
|
||||||
errors["base"] = "invalid_auth"
|
|
||||||
else:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Successfully connected to %s", user_input[CONF_IP_ADDRESS]
|
|
||||||
)
|
|
||||||
|
|
||||||
device_info = await airq.fetch_device_info()
|
device_info = await airq.fetch_device_info()
|
||||||
await self.async_set_unique_id(device_info["id"])
|
await self.async_set_unique_id(device_info["id"])
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(title=device_info["name"], data=user_input)
|
||||||
title=device_info["name"], data=user_input
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairq"],
|
"loggers": ["aioairq"],
|
||||||
"requirements": ["aioairq==0.3.1"]
|
"requirements": ["aioairq==0.3.2"]
|
||||||
}
|
}
|
||||||
|
@ -37,14 +37,14 @@ from .const import (
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AirQEntityDescriptionMixin:
|
class AirQEntityDescriptionMixin:
|
||||||
"""Class for keys required by AirQ entity."""
|
"""Class for keys required by AirQ entity."""
|
||||||
|
|
||||||
value: Callable[[dict], float | int | None]
|
value: Callable[[dict], float | int | None]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AirQEntityDescription(SensorEntityDescription, AirQEntityDescriptionMixin):
|
class AirQEntityDescription(SensorEntityDescription, AirQEntityDescriptionMixin):
|
||||||
"""Describes AirQ sensor entity."""
|
"""Describes AirQ sensor entity."""
|
||||||
|
|
||||||
|
@ -7,12 +7,12 @@ import logging
|
|||||||
from airthings import Airthings, AirthingsError
|
from airthings import Airthings, AirthingsError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import CONF_ID, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import CONF_ID, CONF_SECRET, DOMAIN
|
from .const import CONF_SECRET, DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -8,10 +8,11 @@ import airthings
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_ID
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import CONF_ID, CONF_SECRET, DOMAIN
|
from .const import CONF_SECRET, DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -2,5 +2,4 @@
|
|||||||
|
|
||||||
DOMAIN = "airthings"
|
DOMAIN = "airthings"
|
||||||
|
|
||||||
CONF_ID = "id"
|
|
||||||
CONF_SECRET = "secret"
|
CONF_SECRET = "secret"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Support for airthings ble sensors."""
|
"""Support for airthings ble sensors."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from airthings_ble import AirthingsDevice
|
from airthings_ble import AirthingsDevice
|
||||||
@ -167,10 +168,13 @@ async def async_setup_entry(
|
|||||||
# we need to change some units
|
# we need to change some units
|
||||||
sensors_mapping = SENSORS_MAPPING_TEMPLATE.copy()
|
sensors_mapping = SENSORS_MAPPING_TEMPLATE.copy()
|
||||||
if not is_metric:
|
if not is_metric:
|
||||||
for val in sensors_mapping.values():
|
for key, val in sensors_mapping.items():
|
||||||
if val.native_unit_of_measurement is not VOLUME_BECQUEREL:
|
if val.native_unit_of_measurement is not VOLUME_BECQUEREL:
|
||||||
continue
|
continue
|
||||||
val.native_unit_of_measurement = VOLUME_PICOCURIE
|
sensors_mapping[key] = dataclasses.replace(
|
||||||
|
val,
|
||||||
|
native_unit_of_measurement=VOLUME_PICOCURIE,
|
||||||
|
)
|
||||||
|
|
||||||
entities = []
|
entities = []
|
||||||
_LOGGER.debug("got sensors: %s", coordinator.data.sensors)
|
_LOGGER.debug("got sensors: %s", coordinator.data.sensors)
|
||||||
|
@ -19,6 +19,7 @@ from homeassistant.components import automation
|
|||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
|
CONF_COUNTRY,
|
||||||
CONF_IP_ADDRESS,
|
CONF_IP_ADDRESS,
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
CONF_LONGITUDE,
|
CONF_LONGITUDE,
|
||||||
@ -44,7 +45,6 @@ from homeassistant.helpers.update_coordinator import (
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_CITY,
|
CONF_CITY,
|
||||||
CONF_COUNTRY,
|
|
||||||
CONF_GEOGRAPHIES,
|
CONF_GEOGRAPHIES,
|
||||||
CONF_INTEGRATION_TYPE,
|
CONF_INTEGRATION_TYPE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -19,6 +19,7 @@ from homeassistant import config_entries
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
|
CONF_COUNTRY,
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
CONF_LONGITUDE,
|
CONF_LONGITUDE,
|
||||||
CONF_SHOW_ON_MAP,
|
CONF_SHOW_ON_MAP,
|
||||||
@ -35,7 +36,6 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
|||||||
from . import async_get_geography_id
|
from . import async_get_geography_id
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_CITY,
|
CONF_CITY,
|
||||||
CONF_COUNTRY,
|
|
||||||
CONF_INTEGRATION_TYPE,
|
CONF_INTEGRATION_TYPE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||||
|
@ -9,6 +9,5 @@ INTEGRATION_TYPE_GEOGRAPHY_NAME = "Geographical Location by Name"
|
|||||||
INTEGRATION_TYPE_NODE_PRO = "AirVisual Node/Pro"
|
INTEGRATION_TYPE_NODE_PRO = "AirVisual Node/Pro"
|
||||||
|
|
||||||
CONF_CITY = "city"
|
CONF_CITY = "city"
|
||||||
CONF_COUNTRY = "country"
|
|
||||||
CONF_GEOGRAPHIES = "geographies"
|
CONF_GEOGRAPHIES = "geographies"
|
||||||
CONF_INTEGRATION_TYPE = "integration_type"
|
CONF_INTEGRATION_TYPE = "integration_type"
|
||||||
|
@ -7,6 +7,7 @@ from homeassistant.components.diagnostics import async_redact_data
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
|
CONF_COUNTRY,
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
CONF_LONGITUDE,
|
CONF_LONGITUDE,
|
||||||
CONF_STATE,
|
CONF_STATE,
|
||||||
@ -15,7 +16,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
from .const import CONF_CITY, CONF_COUNTRY, DOMAIN
|
from .const import CONF_CITY, DOMAIN
|
||||||
|
|
||||||
CONF_COORDINATES = "coordinates"
|
CONF_COORDINATES = "coordinates"
|
||||||
CONF_TITLE = "title"
|
CONF_TITLE = "title"
|
||||||
|
@ -15,6 +15,7 @@ from homeassistant.const import (
|
|||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
CONCENTRATION_PARTS_PER_MILLION,
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
CONF_COUNTRY,
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
CONF_LONGITUDE,
|
CONF_LONGITUDE,
|
||||||
CONF_SHOW_ON_MAP,
|
CONF_SHOW_ON_MAP,
|
||||||
@ -25,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
from . import AirVisualEntity
|
from . import AirVisualEntity
|
||||||
from .const import CONF_CITY, CONF_COUNTRY, DOMAIN
|
from .const import CONF_CITY, DOMAIN
|
||||||
|
|
||||||
ATTR_CITY = "city"
|
ATTR_CITY = "city"
|
||||||
ATTR_COUNTRY = "country"
|
ATTR_COUNTRY = "country"
|
||||||
|
@ -26,7 +26,7 @@ from . import AirVisualProData, AirVisualProEntity
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AirVisualProMeasurementKeyMixin:
|
class AirVisualProMeasurementKeyMixin:
|
||||||
"""Define an entity description mixin to include a measurement key."""
|
"""Define an entity description mixin to include a measurement key."""
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ class AirVisualProMeasurementKeyMixin:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AirVisualProMeasurementDescription(
|
class AirVisualProMeasurementDescription(
|
||||||
SensorEntityDescription, AirVisualProMeasurementKeyMixin
|
SensorEntityDescription, AirVisualProMeasurementKeyMixin
|
||||||
):
|
):
|
||||||
|
@ -29,7 +29,7 @@ from .coordinator import AirzoneUpdateCoordinator
|
|||||||
from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity
|
from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
|
class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||||
"""A class that describes airzone binary sensor entities."""
|
"""A class that describes airzone binary sensor entities."""
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ from .coordinator import AirzoneUpdateCoordinator
|
|||||||
from .entity import AirzoneEntity, AirzoneZoneEntity
|
from .entity import AirzoneEntity, AirzoneZoneEntity
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AirzoneSelectDescriptionMixin:
|
class AirzoneSelectDescriptionMixin:
|
||||||
"""Define an entity description mixin for select entities."""
|
"""Define an entity description mixin for select entities."""
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ class AirzoneSelectDescriptionMixin:
|
|||||||
options_dict: dict[str, int]
|
options_dict: dict[str, int]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AirzoneSelectDescription(SelectEntityDescription, AirzoneSelectDescriptionMixin):
|
class AirzoneSelectDescription(SelectEntityDescription, AirzoneSelectDescriptionMixin):
|
||||||
"""Class to describe an Airzone select entity."""
|
"""Class to describe an Airzone select entity."""
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ from .entity import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
|
class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||||
"""A class that describes Airzone Cloud binary sensor entities."""
|
"""A class that describes Airzone Cloud binary sensor entities."""
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING
|
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||||
|
import homeassistant.helpers.device_registry as dr
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
@ -33,6 +34,34 @@ async def async_setup_entry(
|
|||||||
async_add_entities(
|
async_add_entities(
|
||||||
(AladdinDevice(acc, door, config_entry) for door in doors),
|
(AladdinDevice(acc, door, config_entry) for door in doors),
|
||||||
)
|
)
|
||||||
|
remove_stale_devices(hass, config_entry, doors)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_stale_devices(
|
||||||
|
hass: HomeAssistant, config_entry: ConfigEntry, devices: list[dict]
|
||||||
|
) -> None:
|
||||||
|
"""Remove stale devices from device registry."""
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
device_entries = dr.async_entries_for_config_entry(
|
||||||
|
device_registry, config_entry.entry_id
|
||||||
|
)
|
||||||
|
all_device_ids = {f"{door['device_id']}-{door['door_number']}" for door in devices}
|
||||||
|
|
||||||
|
for device_entry in device_entries:
|
||||||
|
device_id: str | None = None
|
||||||
|
|
||||||
|
for identifier in device_entry.identifiers:
|
||||||
|
if identifier[0] == DOMAIN:
|
||||||
|
device_id = identifier[1]
|
||||||
|
break
|
||||||
|
|
||||||
|
if device_id is None or device_id not in all_device_ids:
|
||||||
|
# If device_id is None an invalid device entry was found for this config entry.
|
||||||
|
# If the device_id is not in existing device ids it's a stale device entry.
|
||||||
|
# Remove config entry from this device entry in either case.
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device_entry.id, remove_config_entry_id=config_entry.entry_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AladdinDevice(CoverEntity):
|
class AladdinDevice(CoverEntity):
|
||||||
|
@ -6,5 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aladdin_connect"],
|
"loggers": ["aladdin_connect"],
|
||||||
|
"quality_scale": "platinum",
|
||||||
"requirements": ["AIOAladdinConnect==0.1.58"]
|
"requirements": ["AIOAladdinConnect==0.1.58"]
|
||||||
}
|
}
|
||||||
|
@ -23,14 +23,14 @@ from .const import DOMAIN
|
|||||||
from .model import DoorDevice
|
from .model import DoorDevice
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AccSensorEntityDescriptionMixin:
|
class AccSensorEntityDescriptionMixin:
|
||||||
"""Mixin for required keys."""
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
value_fn: Callable
|
value_fn: Callable
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AccSensorEntityDescription(
|
class AccSensorEntityDescription(
|
||||||
SensorEntityDescription, AccSensorEntityDescriptionMixin
|
SensorEntityDescription, AccSensorEntityDescriptionMixin
|
||||||
):
|
):
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"""Component to interface with an alarm control panel."""
|
"""Component to interface with an alarm control panel."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Final, final
|
from typing import TYPE_CHECKING, Any, Final, final
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -23,26 +23,41 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.config_validation import make_entity_service_schema
|
from homeassistant.helpers.config_validation import make_entity_service_schema
|
||||||
|
from homeassistant.helpers.deprecation import (
|
||||||
|
check_if_deprecated_constant,
|
||||||
|
dir_with_deprecated_constants,
|
||||||
|
)
|
||||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import ( # noqa: F401
|
from .const import ( # noqa: F401
|
||||||
|
_DEPRECATED_FORMAT_NUMBER,
|
||||||
|
_DEPRECATED_FORMAT_TEXT,
|
||||||
|
_DEPRECATED_SUPPORT_ALARM_ARM_AWAY,
|
||||||
|
_DEPRECATED_SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
|
||||||
|
_DEPRECATED_SUPPORT_ALARM_ARM_HOME,
|
||||||
|
_DEPRECATED_SUPPORT_ALARM_ARM_NIGHT,
|
||||||
|
_DEPRECATED_SUPPORT_ALARM_ARM_VACATION,
|
||||||
|
_DEPRECATED_SUPPORT_ALARM_TRIGGER,
|
||||||
ATTR_CHANGED_BY,
|
ATTR_CHANGED_BY,
|
||||||
ATTR_CODE_ARM_REQUIRED,
|
ATTR_CODE_ARM_REQUIRED,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
FORMAT_NUMBER,
|
|
||||||
FORMAT_TEXT,
|
|
||||||
SUPPORT_ALARM_ARM_AWAY,
|
|
||||||
SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
|
|
||||||
SUPPORT_ALARM_ARM_HOME,
|
|
||||||
SUPPORT_ALARM_ARM_NIGHT,
|
|
||||||
SUPPORT_ALARM_ARM_VACATION,
|
|
||||||
SUPPORT_ALARM_TRIGGER,
|
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
CodeFormat,
|
CodeFormat,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from functools import cached_property
|
||||||
|
else:
|
||||||
|
from homeassistant.backports.functools import cached_property
|
||||||
|
|
||||||
|
# As we import constants of the cost module here, we need to add the following
|
||||||
|
# functions to check for deprecated constants again
|
||||||
|
# Both can be removed if no deprecated constant are in this module anymore
|
||||||
|
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||||
|
__dir__ = partial(dir_with_deprecated_constants, module_globals=globals())
|
||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
||||||
@ -121,12 +136,19 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return await component.async_unload_entry(entry)
|
return await component.async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class AlarmControlPanelEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||||
class AlarmControlPanelEntityDescription(EntityDescription):
|
|
||||||
"""A class that describes alarm control panel entities."""
|
"""A class that describes alarm control panel entities."""
|
||||||
|
|
||||||
|
|
||||||
class AlarmControlPanelEntity(Entity):
|
CACHED_PROPERTIES_WITH_ATTR_ = {
|
||||||
|
"code_format",
|
||||||
|
"changed_by",
|
||||||
|
"code_arm_required",
|
||||||
|
"supported_features",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||||
"""An abstract class for alarm control entities."""
|
"""An abstract class for alarm control entities."""
|
||||||
|
|
||||||
entity_description: AlarmControlPanelEntityDescription
|
entity_description: AlarmControlPanelEntityDescription
|
||||||
@ -137,17 +159,17 @@ class AlarmControlPanelEntity(Entity):
|
|||||||
AlarmControlPanelEntityFeature(0)
|
AlarmControlPanelEntityFeature(0)
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def code_format(self) -> CodeFormat | None:
|
def code_format(self) -> CodeFormat | None:
|
||||||
"""Code format or None if no code is required."""
|
"""Code format or None if no code is required."""
|
||||||
return self._attr_code_format
|
return self._attr_code_format
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def changed_by(self) -> str | None:
|
def changed_by(self) -> str | None:
|
||||||
"""Last change triggered by."""
|
"""Last change triggered by."""
|
||||||
return self._attr_changed_by
|
return self._attr_changed_by
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def code_arm_required(self) -> bool:
|
def code_arm_required(self) -> bool:
|
||||||
"""Whether the code is required for arm actions."""
|
"""Whether the code is required for arm actions."""
|
||||||
return self._attr_code_arm_required
|
return self._attr_code_arm_required
|
||||||
@ -208,10 +230,15 @@ class AlarmControlPanelEntity(Entity):
|
|||||||
"""Send arm custom bypass command."""
|
"""Send arm custom bypass command."""
|
||||||
await self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code)
|
await self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code)
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def supported_features(self) -> AlarmControlPanelEntityFeature:
|
def supported_features(self) -> AlarmControlPanelEntityFeature:
|
||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
return self._attr_supported_features
|
features = self._attr_supported_features
|
||||||
|
if type(features) is int: # noqa: E721
|
||||||
|
new_features = AlarmControlPanelEntityFeature(features)
|
||||||
|
self._report_deprecated_supported_features_values(new_features)
|
||||||
|
return new_features
|
||||||
|
return features
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@property
|
@property
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
"""Provides the constants needed for component."""
|
"""Provides the constants needed for component."""
|
||||||
from enum import IntFlag, StrEnum
|
from enum import IntFlag, StrEnum
|
||||||
|
from functools import partial
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
|
from homeassistant.helpers.deprecation import (
|
||||||
|
DeprecatedConstantEnum,
|
||||||
|
check_if_deprecated_constant,
|
||||||
|
dir_with_deprecated_constants,
|
||||||
|
)
|
||||||
|
|
||||||
DOMAIN: Final = "alarm_control_panel"
|
DOMAIN: Final = "alarm_control_panel"
|
||||||
|
|
||||||
ATTR_CHANGED_BY: Final = "changed_by"
|
ATTR_CHANGED_BY: Final = "changed_by"
|
||||||
@ -15,10 +22,10 @@ class CodeFormat(StrEnum):
|
|||||||
NUMBER = "number"
|
NUMBER = "number"
|
||||||
|
|
||||||
|
|
||||||
# These constants are deprecated as of Home Assistant 2022.5
|
# These constants are deprecated as of Home Assistant 2022.5, can be removed in 2025.1
|
||||||
# Please use the CodeFormat enum instead.
|
# Please use the CodeFormat enum instead.
|
||||||
FORMAT_TEXT: Final = "text"
|
_DEPRECATED_FORMAT_TEXT: Final = DeprecatedConstantEnum(CodeFormat.TEXT, "2025.1")
|
||||||
FORMAT_NUMBER: Final = "number"
|
_DEPRECATED_FORMAT_NUMBER: Final = DeprecatedConstantEnum(CodeFormat.NUMBER, "2025.1")
|
||||||
|
|
||||||
|
|
||||||
class AlarmControlPanelEntityFeature(IntFlag):
|
class AlarmControlPanelEntityFeature(IntFlag):
|
||||||
@ -34,12 +41,28 @@ class AlarmControlPanelEntityFeature(IntFlag):
|
|||||||
|
|
||||||
# These constants are deprecated as of Home Assistant 2022.5
|
# These constants are deprecated as of Home Assistant 2022.5
|
||||||
# Please use the AlarmControlPanelEntityFeature enum instead.
|
# Please use the AlarmControlPanelEntityFeature enum instead.
|
||||||
SUPPORT_ALARM_ARM_HOME: Final = 1
|
_DEPRECATED_SUPPORT_ALARM_ARM_HOME: Final = DeprecatedConstantEnum(
|
||||||
SUPPORT_ALARM_ARM_AWAY: Final = 2
|
AlarmControlPanelEntityFeature.ARM_HOME, "2025.1"
|
||||||
SUPPORT_ALARM_ARM_NIGHT: Final = 4
|
)
|
||||||
SUPPORT_ALARM_TRIGGER: Final = 8
|
_DEPRECATED_SUPPORT_ALARM_ARM_AWAY: Final = DeprecatedConstantEnum(
|
||||||
SUPPORT_ALARM_ARM_CUSTOM_BYPASS: Final = 16
|
AlarmControlPanelEntityFeature.ARM_AWAY, "2025.1"
|
||||||
SUPPORT_ALARM_ARM_VACATION: Final = 32
|
)
|
||||||
|
_DEPRECATED_SUPPORT_ALARM_ARM_NIGHT: Final = DeprecatedConstantEnum(
|
||||||
|
AlarmControlPanelEntityFeature.ARM_NIGHT, "2025.1"
|
||||||
|
)
|
||||||
|
_DEPRECATED_SUPPORT_ALARM_TRIGGER: Final = DeprecatedConstantEnum(
|
||||||
|
AlarmControlPanelEntityFeature.TRIGGER, "2025.1"
|
||||||
|
)
|
||||||
|
_DEPRECATED_SUPPORT_ALARM_ARM_CUSTOM_BYPASS: Final = DeprecatedConstantEnum(
|
||||||
|
AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, "2025.1"
|
||||||
|
)
|
||||||
|
_DEPRECATED_SUPPORT_ALARM_ARM_VACATION: Final = DeprecatedConstantEnum(
|
||||||
|
AlarmControlPanelEntityFeature.ARM_VACATION, "2025.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Both can be removed if no deprecated constant are in this module anymore
|
||||||
|
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||||
|
__dir__ = partial(dir_with_deprecated_constants, module_globals=globals())
|
||||||
|
|
||||||
CONDITION_TRIGGERED: Final = "is_triggered"
|
CONDITION_TRIGGERED: Final = "is_triggered"
|
||||||
CONDITION_DISARMED: Final = "is_disarmed"
|
CONDITION_DISARMED: Final = "is_disarmed"
|
||||||
|
@ -28,13 +28,7 @@ from homeassistant.helpers.entity import get_supported_features
|
|||||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||||
|
|
||||||
from . import ATTR_CODE_ARM_REQUIRED, DOMAIN
|
from . import ATTR_CODE_ARM_REQUIRED, DOMAIN
|
||||||
from .const import (
|
from .const import AlarmControlPanelEntityFeature
|
||||||
SUPPORT_ALARM_ARM_AWAY,
|
|
||||||
SUPPORT_ALARM_ARM_HOME,
|
|
||||||
SUPPORT_ALARM_ARM_NIGHT,
|
|
||||||
SUPPORT_ALARM_ARM_VACATION,
|
|
||||||
SUPPORT_ALARM_TRIGGER,
|
|
||||||
)
|
|
||||||
|
|
||||||
ACTION_TYPES: Final[set[str]] = {
|
ACTION_TYPES: Final[set[str]] = {
|
||||||
"arm_away",
|
"arm_away",
|
||||||
@ -82,16 +76,16 @@ async def async_get_actions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Add actions for each entity that belongs to this integration
|
# Add actions for each entity that belongs to this integration
|
||||||
if supported_features & SUPPORT_ALARM_ARM_AWAY:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_AWAY:
|
||||||
actions.append({**base_action, CONF_TYPE: "arm_away"})
|
actions.append({**base_action, CONF_TYPE: "arm_away"})
|
||||||
if supported_features & SUPPORT_ALARM_ARM_HOME:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_HOME:
|
||||||
actions.append({**base_action, CONF_TYPE: "arm_home"})
|
actions.append({**base_action, CONF_TYPE: "arm_home"})
|
||||||
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_NIGHT:
|
||||||
actions.append({**base_action, CONF_TYPE: "arm_night"})
|
actions.append({**base_action, CONF_TYPE: "arm_night"})
|
||||||
if supported_features & SUPPORT_ALARM_ARM_VACATION:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_VACATION:
|
||||||
actions.append({**base_action, CONF_TYPE: "arm_vacation"})
|
actions.append({**base_action, CONF_TYPE: "arm_vacation"})
|
||||||
actions.append({**base_action, CONF_TYPE: "disarm"})
|
actions.append({**base_action, CONF_TYPE: "disarm"})
|
||||||
if supported_features & SUPPORT_ALARM_TRIGGER:
|
if supported_features & AlarmControlPanelEntityFeature.TRIGGER:
|
||||||
actions.append({**base_action, CONF_TYPE: "trigger"})
|
actions.append({**base_action, CONF_TYPE: "trigger"})
|
||||||
|
|
||||||
return actions
|
return actions
|
||||||
|
@ -39,11 +39,7 @@ from .const import (
|
|||||||
CONDITION_ARMED_VACATION,
|
CONDITION_ARMED_VACATION,
|
||||||
CONDITION_DISARMED,
|
CONDITION_DISARMED,
|
||||||
CONDITION_TRIGGERED,
|
CONDITION_TRIGGERED,
|
||||||
SUPPORT_ALARM_ARM_AWAY,
|
AlarmControlPanelEntityFeature,
|
||||||
SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
|
|
||||||
SUPPORT_ALARM_ARM_HOME,
|
|
||||||
SUPPORT_ALARM_ARM_NIGHT,
|
|
||||||
SUPPORT_ALARM_ARM_VACATION,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
CONDITION_TYPES: Final[set[str]] = {
|
CONDITION_TYPES: Final[set[str]] = {
|
||||||
@ -90,15 +86,15 @@ async def async_get_conditions(
|
|||||||
{**base_condition, CONF_TYPE: CONDITION_DISARMED},
|
{**base_condition, CONF_TYPE: CONDITION_DISARMED},
|
||||||
{**base_condition, CONF_TYPE: CONDITION_TRIGGERED},
|
{**base_condition, CONF_TYPE: CONDITION_TRIGGERED},
|
||||||
]
|
]
|
||||||
if supported_features & SUPPORT_ALARM_ARM_HOME:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_HOME:
|
||||||
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_HOME})
|
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_HOME})
|
||||||
if supported_features & SUPPORT_ALARM_ARM_AWAY:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_AWAY:
|
||||||
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_AWAY})
|
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_AWAY})
|
||||||
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_NIGHT:
|
||||||
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_NIGHT})
|
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_NIGHT})
|
||||||
if supported_features & SUPPORT_ALARM_ARM_VACATION:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_VACATION:
|
||||||
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_VACATION})
|
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_VACATION})
|
||||||
if supported_features & SUPPORT_ALARM_ARM_CUSTOM_BYPASS:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS:
|
||||||
conditions.append(
|
conditions.append(
|
||||||
{**base_condition, CONF_TYPE: CONDITION_ARMED_CUSTOM_BYPASS}
|
{**base_condition, CONF_TYPE: CONDITION_ARMED_CUSTOM_BYPASS}
|
||||||
)
|
)
|
||||||
|
@ -29,12 +29,7 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
|||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from . import DOMAIN
|
from . import DOMAIN
|
||||||
from .const import (
|
from .const import AlarmControlPanelEntityFeature
|
||||||
SUPPORT_ALARM_ARM_AWAY,
|
|
||||||
SUPPORT_ALARM_ARM_HOME,
|
|
||||||
SUPPORT_ALARM_ARM_NIGHT,
|
|
||||||
SUPPORT_ALARM_ARM_VACATION,
|
|
||||||
)
|
|
||||||
|
|
||||||
BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"}
|
BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"}
|
||||||
TRIGGER_TYPES: Final[set[str]] = BASIC_TRIGGER_TYPES | {
|
TRIGGER_TYPES: Final[set[str]] = BASIC_TRIGGER_TYPES | {
|
||||||
@ -82,28 +77,28 @@ async def async_get_triggers(
|
|||||||
}
|
}
|
||||||
for trigger in BASIC_TRIGGER_TYPES
|
for trigger in BASIC_TRIGGER_TYPES
|
||||||
]
|
]
|
||||||
if supported_features & SUPPORT_ALARM_ARM_HOME:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_HOME:
|
||||||
triggers.append(
|
triggers.append(
|
||||||
{
|
{
|
||||||
**base_trigger,
|
**base_trigger,
|
||||||
CONF_TYPE: "armed_home",
|
CONF_TYPE: "armed_home",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if supported_features & SUPPORT_ALARM_ARM_AWAY:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_AWAY:
|
||||||
triggers.append(
|
triggers.append(
|
||||||
{
|
{
|
||||||
**base_trigger,
|
**base_trigger,
|
||||||
CONF_TYPE: "armed_away",
|
CONF_TYPE: "armed_away",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_NIGHT:
|
||||||
triggers.append(
|
triggers.append(
|
||||||
{
|
{
|
||||||
**base_trigger,
|
**base_trigger,
|
||||||
CONF_TYPE: "armed_night",
|
CONF_TYPE: "armed_night",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if supported_features & SUPPORT_ALARM_ARM_VACATION:
|
if supported_features & AlarmControlPanelEntityFeature.ARM_VACATION:
|
||||||
triggers.append(
|
triggers.append(
|
||||||
{
|
{
|
||||||
**base_trigger,
|
**base_trigger,
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
"""Helper to test significant Alarm Control Panel state changes."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from . import ATTR_CHANGED_BY, ATTR_CODE_ARM_REQUIRED
|
||||||
|
|
||||||
|
SIGNIFICANT_ATTRIBUTES: set[str] = {
|
||||||
|
ATTR_CHANGED_BY,
|
||||||
|
ATTR_CODE_ARM_REQUIRED,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_check_significant_change(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
old_state: str,
|
||||||
|
old_attrs: dict,
|
||||||
|
new_state: str,
|
||||||
|
new_attrs: dict,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> bool | None:
|
||||||
|
"""Test if state significantly changed."""
|
||||||
|
if old_state != new_state:
|
||||||
|
return True
|
||||||
|
|
||||||
|
old_attrs_s = set(
|
||||||
|
{k: v for k, v in old_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items()
|
||||||
|
)
|
||||||
|
new_attrs_s = set(
|
||||||
|
{k: v for k, v in new_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items()
|
||||||
|
)
|
||||||
|
changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s}
|
||||||
|
|
||||||
|
if changed_attrs:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# no significant attribute change detected
|
||||||
|
return False
|
@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from adext import AdExt
|
from adext import AdExt
|
||||||
from alarmdecoder.devices import SerialDevice, SocketDevice
|
from alarmdecoder.devices import SerialDevice, SocketDevice
|
||||||
@ -12,8 +13,10 @@ from homeassistant import config_entries
|
|||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
|
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL
|
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ALT_NIGHT_MODE,
|
CONF_ALT_NIGHT_MODE,
|
||||||
@ -66,7 +69,9 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"""Get the options flow for AlarmDecoder."""
|
"""Get the options flow for AlarmDecoder."""
|
||||||
return AlarmDecoderOptionsFlowHandler(config_entry)
|
return AlarmDecoderOptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
"""Handle a flow initialized by the user."""
|
"""Handle a flow initialized by the user."""
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self.protocol = user_input[CONF_PROTOCOL]
|
self.protocol = user_input[CONF_PROTOCOL]
|
||||||
@ -83,7 +88,9 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_protocol(self, user_input=None):
|
async def async_step_protocol(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
"""Handle AlarmDecoder protocol setup."""
|
"""Handle AlarmDecoder protocol setup."""
|
||||||
errors = {}
|
errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
@ -146,15 +153,18 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
|
class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
"""Handle AlarmDecoder options."""
|
"""Handle AlarmDecoder options."""
|
||||||
|
|
||||||
|
selected_zone: str | None = None
|
||||||
|
|
||||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
"""Initialize AlarmDecoder options flow."""
|
"""Initialize AlarmDecoder options flow."""
|
||||||
self.arm_options = config_entry.options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS)
|
self.arm_options = config_entry.options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS)
|
||||||
self.zone_options = config_entry.options.get(
|
self.zone_options = config_entry.options.get(
|
||||||
OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS
|
OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS
|
||||||
)
|
)
|
||||||
self.selected_zone = None
|
|
||||||
|
|
||||||
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."""
|
"""Manage the options."""
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
if user_input[EDIT_KEY] == EDIT_SETTINGS:
|
if user_input[EDIT_KEY] == EDIT_SETTINGS:
|
||||||
@ -173,7 +183,9 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_arm_settings(self, user_input=None):
|
async def async_step_arm_settings(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
"""Arming options form."""
|
"""Arming options form."""
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
@ -200,7 +212,9 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_zone_select(self, user_input=None):
|
async def async_step_zone_select(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
"""Zone selection form."""
|
"""Zone selection form."""
|
||||||
errors = _validate_zone_input(user_input)
|
errors = _validate_zone_input(user_input)
|
||||||
|
|
||||||
@ -216,7 +230,9 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_zone_details(self, user_input=None):
|
async def async_step_zone_details(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
"""Zone details form."""
|
"""Zone details form."""
|
||||||
errors = _validate_zone_input(user_input)
|
errors = _validate_zone_input(user_input)
|
||||||
|
|
||||||
@ -293,7 +309,7 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_zone_input(zone_input):
|
def _validate_zone_input(zone_input: dict[str, Any] | None) -> dict[str, str]:
|
||||||
if not zone_input:
|
if not zone_input:
|
||||||
return {}
|
return {}
|
||||||
errors = {}
|
errors = {}
|
||||||
@ -327,7 +343,7 @@ def _validate_zone_input(zone_input):
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def _fix_input_types(zone_input):
|
def _fix_input_types(zone_input: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Convert necessary keys to int.
|
"""Convert necessary keys to int.
|
||||||
|
|
||||||
Since ConfigFlow inputs of type int cannot default to an empty string, we collect the values below as
|
Since ConfigFlow inputs of type int cannot default to an empty string, we collect the values below as
|
||||||
@ -341,7 +357,9 @@ def _fix_input_types(zone_input):
|
|||||||
return zone_input
|
return zone_input
|
||||||
|
|
||||||
|
|
||||||
def _device_already_added(current_entries, user_input, protocol):
|
def _device_already_added(
|
||||||
|
current_entries: list[ConfigEntry], user_input: dict[str, Any], protocol: str | None
|
||||||
|
) -> bool:
|
||||||
"""Determine if entry has already been added to HA."""
|
"""Determine if entry has already been added to HA."""
|
||||||
user_host = user_input.get(CONF_HOST)
|
user_host = user_input.get(CONF_HOST)
|
||||||
user_port = user_input.get(CONF_PORT)
|
user_port = user_input.get(CONF_PORT)
|
||||||
|
@ -36,6 +36,15 @@ CONF_FLASH_BRIEFINGS = "flash_briefings"
|
|||||||
CONF_SMART_HOME = "smart_home"
|
CONF_SMART_HOME = "smart_home"
|
||||||
DEFAULT_LOCALE = "en-US"
|
DEFAULT_LOCALE = "en-US"
|
||||||
|
|
||||||
|
# Alexa Smart Home API send events gateway endpoints
|
||||||
|
# https://developer.amazon.com/en-US/docs/alexa/smarthome/send-events.html#endpoints
|
||||||
|
VALID_ENDPOINTS = [
|
||||||
|
"https://api.amazonalexa.com/v3/events",
|
||||||
|
"https://api.eu.amazonalexa.com/v3/events",
|
||||||
|
"https://api.fe.amazonalexa.com/v3/events",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
ALEXA_ENTITY_SCHEMA = vol.Schema(
|
ALEXA_ENTITY_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_DESCRIPTION): cv.string,
|
vol.Optional(CONF_DESCRIPTION): cv.string,
|
||||||
@ -46,7 +55,7 @@ ALEXA_ENTITY_SCHEMA = vol.Schema(
|
|||||||
|
|
||||||
SMART_HOME_SCHEMA = vol.Schema(
|
SMART_HOME_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_ENDPOINT): cv.string,
|
vol.Optional(CONF_ENDPOINT): vol.All(vol.Lower, vol.In(VALID_ENDPOINTS)),
|
||||||
vol.Optional(CONF_CLIENT_ID): cv.string,
|
vol.Optional(CONF_CLIENT_ID): cv.string,
|
||||||
vol.Optional(CONF_CLIENT_SECRET): cv.string,
|
vol.Optional(CONF_CLIENT_SECRET): cv.string,
|
||||||
vol.Optional(CONF_LOCALE, default=DEFAULT_LOCALE): vol.In(
|
vol.Optional(CONF_LOCALE, default=DEFAULT_LOCALE): vol.In(
|
||||||
|
@ -19,6 +19,8 @@ from homeassistant.components import (
|
|||||||
number,
|
number,
|
||||||
timer,
|
timer,
|
||||||
vacuum,
|
vacuum,
|
||||||
|
valve,
|
||||||
|
water_heater,
|
||||||
)
|
)
|
||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
@ -435,7 +437,8 @@ class AlexaPowerController(AlexaCapability):
|
|||||||
is_on = self.entity.state == vacuum.STATE_CLEANING
|
is_on = self.entity.state == vacuum.STATE_CLEANING
|
||||||
elif self.entity.domain == timer.DOMAIN:
|
elif self.entity.domain == timer.DOMAIN:
|
||||||
is_on = self.entity.state != STATE_IDLE
|
is_on = self.entity.state != STATE_IDLE
|
||||||
|
elif self.entity.domain == water_heater.DOMAIN:
|
||||||
|
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
|
||||||
else:
|
else:
|
||||||
is_on = self.entity.state != STATE_OFF
|
is_on = self.entity.state != STATE_OFF
|
||||||
|
|
||||||
@ -938,6 +941,9 @@ class AlexaTemperatureSensor(AlexaCapability):
|
|||||||
if self.entity.domain == climate.DOMAIN:
|
if self.entity.domain == climate.DOMAIN:
|
||||||
unit = self.hass.config.units.temperature_unit
|
unit = self.hass.config.units.temperature_unit
|
||||||
temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)
|
temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)
|
||||||
|
elif self.entity.domain == water_heater.DOMAIN:
|
||||||
|
unit = self.hass.config.units.temperature_unit
|
||||||
|
temp = self.entity.attributes.get(water_heater.ATTR_CURRENT_TEMPERATURE)
|
||||||
|
|
||||||
if temp is None or temp in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
if temp is None or temp in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||||
return None
|
return None
|
||||||
@ -1108,6 +1114,8 @@ class AlexaThermostatController(AlexaCapability):
|
|||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE:
|
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE:
|
||||||
properties.append({"name": "targetSetpoint"})
|
properties.append({"name": "targetSetpoint"})
|
||||||
|
if supported & water_heater.WaterHeaterEntityFeature.TARGET_TEMPERATURE:
|
||||||
|
properties.append({"name": "targetSetpoint"})
|
||||||
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
||||||
properties.append({"name": "lowerSetpoint"})
|
properties.append({"name": "lowerSetpoint"})
|
||||||
properties.append({"name": "upperSetpoint"})
|
properties.append({"name": "upperSetpoint"})
|
||||||
@ -1127,6 +1135,8 @@ class AlexaThermostatController(AlexaCapability):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if name == "thermostatMode":
|
if name == "thermostatMode":
|
||||||
|
if self.entity.domain == water_heater.DOMAIN:
|
||||||
|
return None
|
||||||
preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE)
|
preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE)
|
||||||
|
|
||||||
mode: dict[str, str] | str | None
|
mode: dict[str, str] | str | None
|
||||||
@ -1176,9 +1186,13 @@ class AlexaThermostatController(AlexaCapability):
|
|||||||
ThermostatMode Values.
|
ThermostatMode Values.
|
||||||
|
|
||||||
ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM.
|
ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM.
|
||||||
|
Water heater devices do not return thermostat modes.
|
||||||
"""
|
"""
|
||||||
|
if self.entity.domain == water_heater.DOMAIN:
|
||||||
|
return None
|
||||||
|
|
||||||
supported_modes: list[str] = []
|
supported_modes: list[str] = []
|
||||||
hvac_modes = self.entity.attributes[climate.ATTR_HVAC_MODES]
|
hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES, [])
|
||||||
for mode in hvac_modes:
|
for mode in hvac_modes:
|
||||||
if thermostat_mode := API_THERMOSTAT_MODES.get(mode):
|
if thermostat_mode := API_THERMOSTAT_MODES.get(mode):
|
||||||
supported_modes.append(thermostat_mode)
|
supported_modes.append(thermostat_mode)
|
||||||
@ -1408,6 +1422,16 @@ class AlexaModeController(AlexaCapability):
|
|||||||
if mode in self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []):
|
if mode in self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []):
|
||||||
return f"{humidifier.ATTR_MODE}.{mode}"
|
return f"{humidifier.ATTR_MODE}.{mode}"
|
||||||
|
|
||||||
|
# Water heater operation mode
|
||||||
|
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
|
||||||
|
operation_mode = self.entity.attributes.get(
|
||||||
|
water_heater.ATTR_OPERATION_MODE, None
|
||||||
|
)
|
||||||
|
if operation_mode in self.entity.attributes.get(
|
||||||
|
water_heater.ATTR_OPERATION_LIST, []
|
||||||
|
):
|
||||||
|
return f"{water_heater.ATTR_OPERATION_MODE}.{operation_mode}"
|
||||||
|
|
||||||
# Cover Position
|
# Cover Position
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
# Return state instead of position when using ModeController.
|
# Return state instead of position when using ModeController.
|
||||||
@ -1421,6 +1445,19 @@ class AlexaModeController(AlexaCapability):
|
|||||||
):
|
):
|
||||||
return f"{cover.ATTR_POSITION}.{mode}"
|
return f"{cover.ATTR_POSITION}.{mode}"
|
||||||
|
|
||||||
|
# Valve position state
|
||||||
|
if self.instance == f"{valve.DOMAIN}.state":
|
||||||
|
# Return state instead of position when using ModeController.
|
||||||
|
state = self.entity.state
|
||||||
|
if state in (
|
||||||
|
valve.STATE_OPEN,
|
||||||
|
valve.STATE_OPENING,
|
||||||
|
valve.STATE_CLOSED,
|
||||||
|
valve.STATE_CLOSING,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
):
|
||||||
|
return f"state.{state}"
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def configuration(self) -> dict[str, Any] | None:
|
def configuration(self) -> dict[str, Any] | None:
|
||||||
@ -1478,6 +1515,26 @@ class AlexaModeController(AlexaCapability):
|
|||||||
)
|
)
|
||||||
return self._resource.serialize_capability_resources()
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
|
# Water heater operation modes
|
||||||
|
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
|
||||||
|
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
|
||||||
|
operation_modes = self.entity.attributes.get(
|
||||||
|
water_heater.ATTR_OPERATION_LIST, []
|
||||||
|
)
|
||||||
|
for operation_mode in operation_modes:
|
||||||
|
self._resource.add_mode(
|
||||||
|
f"{water_heater.ATTR_OPERATION_MODE}.{operation_mode}",
|
||||||
|
[operation_mode],
|
||||||
|
)
|
||||||
|
# Devices with a single mode completely break Alexa discovery,
|
||||||
|
# add a fake preset (see issue #53832).
|
||||||
|
if len(operation_modes) == 1:
|
||||||
|
self._resource.add_mode(
|
||||||
|
f"{water_heater.ATTR_OPERATION_MODE}.{PRESET_MODE_NA}",
|
||||||
|
[PRESET_MODE_NA],
|
||||||
|
)
|
||||||
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
# Cover Position Resources
|
# Cover Position Resources
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
self._resource = AlexaModeResource(
|
self._resource = AlexaModeResource(
|
||||||
@ -1497,6 +1554,32 @@ class AlexaModeController(AlexaCapability):
|
|||||||
)
|
)
|
||||||
return self._resource.serialize_capability_resources()
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
|
# Valve position resources
|
||||||
|
if self.instance == f"{valve.DOMAIN}.state":
|
||||||
|
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
self._resource = AlexaModeResource(
|
||||||
|
["Preset", AlexaGlobalCatalog.SETTING_PRESET], False
|
||||||
|
)
|
||||||
|
modes = 0
|
||||||
|
if supported_features & valve.ValveEntityFeature.OPEN:
|
||||||
|
self._resource.add_mode(
|
||||||
|
f"state.{valve.STATE_OPEN}",
|
||||||
|
["Open", AlexaGlobalCatalog.SETTING_PRESET],
|
||||||
|
)
|
||||||
|
modes += 1
|
||||||
|
if supported_features & valve.ValveEntityFeature.CLOSE:
|
||||||
|
self._resource.add_mode(
|
||||||
|
f"state.{valve.STATE_CLOSED}",
|
||||||
|
["Closed", AlexaGlobalCatalog.SETTING_PRESET],
|
||||||
|
)
|
||||||
|
modes += 1
|
||||||
|
|
||||||
|
# Alexa requiers at least 2 modes
|
||||||
|
if modes == 1:
|
||||||
|
self._resource.add_mode(f"state.{PRESET_MODE_NA}", [PRESET_MODE_NA])
|
||||||
|
|
||||||
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def semantics(self) -> dict[str, Any] | None:
|
def semantics(self) -> dict[str, Any] | None:
|
||||||
@ -1535,6 +1618,34 @@ class AlexaModeController(AlexaCapability):
|
|||||||
|
|
||||||
return self._semantics.serialize_semantics()
|
return self._semantics.serialize_semantics()
|
||||||
|
|
||||||
|
# Valve Position
|
||||||
|
if self.instance == f"{valve.DOMAIN}.state":
|
||||||
|
close_labels = [AlexaSemantics.ACTION_CLOSE]
|
||||||
|
open_labels = [AlexaSemantics.ACTION_OPEN]
|
||||||
|
self._semantics = AlexaSemantics()
|
||||||
|
|
||||||
|
self._semantics.add_states_to_value(
|
||||||
|
[AlexaSemantics.STATES_CLOSED],
|
||||||
|
f"state.{valve.STATE_CLOSED}",
|
||||||
|
)
|
||||||
|
self._semantics.add_states_to_value(
|
||||||
|
[AlexaSemantics.STATES_OPEN],
|
||||||
|
f"state.{valve.STATE_OPEN}",
|
||||||
|
)
|
||||||
|
|
||||||
|
self._semantics.add_action_to_directive(
|
||||||
|
close_labels,
|
||||||
|
"SetMode",
|
||||||
|
{"mode": f"state.{valve.STATE_CLOSED}"},
|
||||||
|
)
|
||||||
|
self._semantics.add_action_to_directive(
|
||||||
|
open_labels,
|
||||||
|
"SetMode",
|
||||||
|
{"mode": f"state.{valve.STATE_OPEN}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._semantics.serialize_semantics()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -1648,6 +1759,10 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
)
|
)
|
||||||
return speed_index
|
return speed_index
|
||||||
|
|
||||||
|
# Valve Position
|
||||||
|
if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
|
||||||
|
return self.entity.attributes.get(valve.ATTR_CURRENT_POSITION)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def configuration(self) -> dict[str, Any] | None:
|
def configuration(self) -> dict[str, Any] | None:
|
||||||
@ -1771,6 +1886,17 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
|
|
||||||
return self._resource.serialize_capability_resources()
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
|
# Valve Position Resources
|
||||||
|
if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
|
||||||
|
self._resource = AlexaPresetResource(
|
||||||
|
["Opening", AlexaGlobalCatalog.SETTING_OPENING],
|
||||||
|
min_value=0,
|
||||||
|
max_value=100,
|
||||||
|
precision=1,
|
||||||
|
unit=AlexaGlobalCatalog.UNIT_PERCENT,
|
||||||
|
)
|
||||||
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def semantics(self) -> dict[str, Any] | None:
|
def semantics(self) -> dict[str, Any] | None:
|
||||||
@ -1847,6 +1973,25 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
)
|
)
|
||||||
return self._semantics.serialize_semantics()
|
return self._semantics.serialize_semantics()
|
||||||
|
|
||||||
|
# Valve Position
|
||||||
|
if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
|
||||||
|
close_labels = [AlexaSemantics.ACTION_CLOSE]
|
||||||
|
open_labels = [AlexaSemantics.ACTION_OPEN]
|
||||||
|
self._semantics = AlexaSemantics()
|
||||||
|
|
||||||
|
self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0)
|
||||||
|
self._semantics.add_states_to_range(
|
||||||
|
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
|
||||||
|
)
|
||||||
|
|
||||||
|
self._semantics.add_action_to_directive(
|
||||||
|
close_labels, "SetRangeValue", {"rangeValue": 0}
|
||||||
|
)
|
||||||
|
self._semantics.add_action_to_directive(
|
||||||
|
open_labels, "SetRangeValue", {"rangeValue": 100}
|
||||||
|
)
|
||||||
|
return self._semantics.serialize_semantics()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -1920,6 +2065,10 @@ class AlexaToggleController(AlexaCapability):
|
|||||||
is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING))
|
is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING))
|
||||||
return "ON" if is_on else "OFF"
|
return "ON" if is_on else "OFF"
|
||||||
|
|
||||||
|
# Stop Valve
|
||||||
|
if self.instance == f"{valve.DOMAIN}.stop":
|
||||||
|
return "OFF"
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def capability_resources(self) -> dict[str, list[dict[str, Any]]]:
|
def capability_resources(self) -> dict[str, list[dict[str, Any]]]:
|
||||||
@ -1932,6 +2081,10 @@ class AlexaToggleController(AlexaCapability):
|
|||||||
)
|
)
|
||||||
return self._resource.serialize_capability_resources()
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
|
if self.instance == f"{valve.DOMAIN}.stop":
|
||||||
|
self._resource = AlexaCapabilityResource(["Stop"])
|
||||||
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +32,8 @@ from homeassistant.components import (
|
|||||||
switch,
|
switch,
|
||||||
timer,
|
timer,
|
||||||
vacuum,
|
vacuum,
|
||||||
|
valve,
|
||||||
|
water_heater,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_DEVICE_CLASS,
|
ATTR_DEVICE_CLASS,
|
||||||
@ -248,6 +250,9 @@ class DisplayCategory:
|
|||||||
# Indicates a vacuum cleaner.
|
# Indicates a vacuum cleaner.
|
||||||
VACUUM_CLEANER = "VACUUM_CLEANER"
|
VACUUM_CLEANER = "VACUUM_CLEANER"
|
||||||
|
|
||||||
|
# Indicates a water heater.
|
||||||
|
WATER_HEATER = "WATER_HEATER"
|
||||||
|
|
||||||
# Indicates a network-connected wearable device, such as an Apple Watch,
|
# Indicates a network-connected wearable device, such as an Apple Watch,
|
||||||
# Fitbit, or Samsung Gear.
|
# Fitbit, or Samsung Gear.
|
||||||
WEARABLE = "WEARABLE"
|
WEARABLE = "WEARABLE"
|
||||||
@ -456,23 +461,46 @@ class ButtonCapabilities(AlexaEntity):
|
|||||||
|
|
||||||
|
|
||||||
@ENTITY_ADAPTERS.register(climate.DOMAIN)
|
@ENTITY_ADAPTERS.register(climate.DOMAIN)
|
||||||
|
@ENTITY_ADAPTERS.register(water_heater.DOMAIN)
|
||||||
class ClimateCapabilities(AlexaEntity):
|
class ClimateCapabilities(AlexaEntity):
|
||||||
"""Class to represent Climate capabilities."""
|
"""Class to represent Climate capabilities."""
|
||||||
|
|
||||||
def default_display_categories(self) -> list[str]:
|
def default_display_categories(self) -> list[str]:
|
||||||
"""Return the display categories for this entity."""
|
"""Return the display categories for this entity."""
|
||||||
|
if self.entity.domain == water_heater.DOMAIN:
|
||||||
|
return [DisplayCategory.WATER_HEATER]
|
||||||
return [DisplayCategory.THERMOSTAT]
|
return [DisplayCategory.THERMOSTAT]
|
||||||
|
|
||||||
def interfaces(self) -> Generator[AlexaCapability, None, None]:
|
def interfaces(self) -> Generator[AlexaCapability, None, None]:
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
# If we support two modes, one being off, we allow turning on too.
|
# If we support two modes, one being off, we allow turning on too.
|
||||||
if climate.HVACMode.OFF in self.entity.attributes.get(
|
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
climate.ATTR_HVAC_MODES, []
|
if (
|
||||||
|
self.entity.domain == climate.DOMAIN
|
||||||
|
and climate.HVACMode.OFF
|
||||||
|
in self.entity.attributes.get(climate.ATTR_HVAC_MODES, [])
|
||||||
|
or self.entity.domain == water_heater.DOMAIN
|
||||||
|
and (supported_features & water_heater.WaterHeaterEntityFeature.ON_OFF)
|
||||||
):
|
):
|
||||||
yield AlexaPowerController(self.entity)
|
yield AlexaPowerController(self.entity)
|
||||||
|
|
||||||
yield AlexaThermostatController(self.hass, self.entity)
|
if (
|
||||||
yield AlexaTemperatureSensor(self.hass, self.entity)
|
self.entity.domain == climate.DOMAIN
|
||||||
|
or self.entity.domain == water_heater.DOMAIN
|
||||||
|
and (
|
||||||
|
supported_features
|
||||||
|
& water_heater.WaterHeaterEntityFeature.OPERATION_MODE
|
||||||
|
)
|
||||||
|
):
|
||||||
|
yield AlexaThermostatController(self.hass, self.entity)
|
||||||
|
yield AlexaTemperatureSensor(self.hass, self.entity)
|
||||||
|
if self.entity.domain == water_heater.DOMAIN and (
|
||||||
|
supported_features & water_heater.WaterHeaterEntityFeature.OPERATION_MODE
|
||||||
|
):
|
||||||
|
yield AlexaModeController(
|
||||||
|
self.entity,
|
||||||
|
instance=f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}",
|
||||||
|
)
|
||||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
yield Alexa(self.entity)
|
yield Alexa(self.entity)
|
||||||
|
|
||||||
@ -949,6 +977,31 @@ class VacuumCapabilities(AlexaEntity):
|
|||||||
yield Alexa(self.entity)
|
yield Alexa(self.entity)
|
||||||
|
|
||||||
|
|
||||||
|
@ENTITY_ADAPTERS.register(valve.DOMAIN)
|
||||||
|
class ValveCapabilities(AlexaEntity):
|
||||||
|
"""Class to represent Valve capabilities."""
|
||||||
|
|
||||||
|
def default_display_categories(self) -> list[str]:
|
||||||
|
"""Return the display categories for this entity."""
|
||||||
|
return [DisplayCategory.OTHER]
|
||||||
|
|
||||||
|
def interfaces(self) -> Generator[AlexaCapability, None, None]:
|
||||||
|
"""Yield the supported interfaces."""
|
||||||
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
if supported & valve.ValveEntityFeature.SET_POSITION:
|
||||||
|
yield AlexaRangeController(
|
||||||
|
self.entity, instance=f"{valve.DOMAIN}.{valve.ATTR_POSITION}"
|
||||||
|
)
|
||||||
|
elif supported & (
|
||||||
|
valve.ValveEntityFeature.CLOSE | valve.ValveEntityFeature.OPEN
|
||||||
|
):
|
||||||
|
yield AlexaModeController(self.entity, instance=f"{valve.DOMAIN}.state")
|
||||||
|
if supported & valve.ValveEntityFeature.STOP:
|
||||||
|
yield AlexaToggleController(self.entity, instance=f"{valve.DOMAIN}.stop")
|
||||||
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
|
yield Alexa(self.entity)
|
||||||
|
|
||||||
|
|
||||||
@ENTITY_ADAPTERS.register(camera.DOMAIN)
|
@ENTITY_ADAPTERS.register(camera.DOMAIN)
|
||||||
class CameraCapabilities(AlexaEntity):
|
class CameraCapabilities(AlexaEntity):
|
||||||
"""Class to represent Camera capabilities."""
|
"""Class to represent Camera capabilities."""
|
||||||
|
@ -22,6 +22,8 @@ from homeassistant.components import (
|
|||||||
number,
|
number,
|
||||||
timer,
|
timer,
|
||||||
vacuum,
|
vacuum,
|
||||||
|
valve,
|
||||||
|
water_heater,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
@ -80,6 +82,23 @@ from .state_report import AlexaDirective, AlexaResponse, async_enable_proactive_
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DIRECTIVE_NOT_SUPPORTED = "Entity does not support directive"
|
DIRECTIVE_NOT_SUPPORTED = "Entity does not support directive"
|
||||||
|
|
||||||
|
MIN_MAX_TEMP = {
|
||||||
|
climate.DOMAIN: {
|
||||||
|
"min_temp": climate.ATTR_MIN_TEMP,
|
||||||
|
"max_temp": climate.ATTR_MAX_TEMP,
|
||||||
|
},
|
||||||
|
water_heater.DOMAIN: {
|
||||||
|
"min_temp": water_heater.ATTR_MIN_TEMP,
|
||||||
|
"max_temp": water_heater.ATTR_MAX_TEMP,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
SERVICE_SET_TEMPERATURE = {
|
||||||
|
climate.DOMAIN: climate.SERVICE_SET_TEMPERATURE,
|
||||||
|
water_heater.DOMAIN: water_heater.SERVICE_SET_TEMPERATURE,
|
||||||
|
}
|
||||||
|
|
||||||
HANDLERS: Registry[
|
HANDLERS: Registry[
|
||||||
tuple[str, str],
|
tuple[str, str],
|
||||||
Callable[
|
Callable[
|
||||||
@ -804,8 +823,10 @@ async def async_api_set_target_temp(
|
|||||||
) -> AlexaResponse:
|
) -> AlexaResponse:
|
||||||
"""Process a set target temperature request."""
|
"""Process a set target temperature request."""
|
||||||
entity = directive.entity
|
entity = directive.entity
|
||||||
min_temp = entity.attributes[climate.ATTR_MIN_TEMP]
|
domain = entity.domain
|
||||||
max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
|
|
||||||
|
min_temp = entity.attributes[MIN_MAX_TEMP[domain]["min_temp"]]
|
||||||
|
max_temp = entity.attributes["max_temp"]
|
||||||
unit = hass.config.units.temperature_unit
|
unit = hass.config.units.temperature_unit
|
||||||
|
|
||||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||||
@ -849,9 +870,11 @@ async def async_api_set_target_temp(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
service = SERVICE_SET_TEMPERATURE[domain]
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
entity.domain,
|
entity.domain,
|
||||||
climate.SERVICE_SET_TEMPERATURE,
|
service,
|
||||||
data,
|
data,
|
||||||
blocking=False,
|
blocking=False,
|
||||||
context=context,
|
context=context,
|
||||||
@ -867,11 +890,12 @@ async def async_api_adjust_target_temp(
|
|||||||
directive: AlexaDirective,
|
directive: AlexaDirective,
|
||||||
context: ha.Context,
|
context: ha.Context,
|
||||||
) -> AlexaResponse:
|
) -> AlexaResponse:
|
||||||
"""Process an adjust target temperature request."""
|
"""Process an adjust target temperature request for climates and water heaters."""
|
||||||
data: dict[str, Any]
|
data: dict[str, Any]
|
||||||
entity = directive.entity
|
entity = directive.entity
|
||||||
min_temp = entity.attributes[climate.ATTR_MIN_TEMP]
|
domain = entity.domain
|
||||||
max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
|
min_temp = entity.attributes[MIN_MAX_TEMP[domain]["min_temp"]]
|
||||||
|
max_temp = entity.attributes[MIN_MAX_TEMP[domain]["max_temp"]]
|
||||||
unit = hass.config.units.temperature_unit
|
unit = hass.config.units.temperature_unit
|
||||||
|
|
||||||
temp_delta = temperature_from_object(
|
temp_delta = temperature_from_object(
|
||||||
@ -932,9 +956,11 @@ async def async_api_adjust_target_temp(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
service = SERVICE_SET_TEMPERATURE[domain]
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
entity.domain,
|
entity.domain,
|
||||||
climate.SERVICE_SET_TEMPERATURE,
|
service,
|
||||||
data,
|
data,
|
||||||
blocking=False,
|
blocking=False,
|
||||||
context=context,
|
context=context,
|
||||||
@ -1163,6 +1189,23 @@ async def async_api_set_mode(
|
|||||||
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
|
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
|
||||||
raise AlexaInvalidValueError(msg)
|
raise AlexaInvalidValueError(msg)
|
||||||
|
|
||||||
|
# Water heater operation mode
|
||||||
|
elif instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
|
||||||
|
operation_mode = mode.split(".")[1]
|
||||||
|
operation_modes: list[str] | None = entity.attributes.get(
|
||||||
|
water_heater.ATTR_OPERATION_LIST
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
operation_mode != PRESET_MODE_NA
|
||||||
|
and operation_modes
|
||||||
|
and operation_mode in operation_modes
|
||||||
|
):
|
||||||
|
service = water_heater.SERVICE_SET_OPERATION_MODE
|
||||||
|
data[water_heater.ATTR_OPERATION_MODE] = operation_mode
|
||||||
|
else:
|
||||||
|
msg = f"Entity '{entity.entity_id}' does not support Operation mode '{operation_mode}'"
|
||||||
|
raise AlexaInvalidValueError(msg)
|
||||||
|
|
||||||
# Cover Position
|
# Cover Position
|
||||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
position = mode.split(".")[1]
|
position = mode.split(".")[1]
|
||||||
@ -1174,6 +1217,15 @@ async def async_api_set_mode(
|
|||||||
elif position == "custom":
|
elif position == "custom":
|
||||||
service = cover.SERVICE_STOP_COVER
|
service = cover.SERVICE_STOP_COVER
|
||||||
|
|
||||||
|
# Valve position state
|
||||||
|
elif instance == f"{valve.DOMAIN}.state":
|
||||||
|
position = mode.split(".")[1]
|
||||||
|
|
||||||
|
if position == valve.STATE_CLOSED:
|
||||||
|
service = valve.SERVICE_CLOSE_VALVE
|
||||||
|
elif position == valve.STATE_OPEN:
|
||||||
|
service = valve.SERVICE_OPEN_VALVE
|
||||||
|
|
||||||
if not service:
|
if not service:
|
||||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||||
|
|
||||||
@ -1224,15 +1276,22 @@ async def async_api_toggle_on(
|
|||||||
instance = directive.instance
|
instance = directive.instance
|
||||||
domain = entity.domain
|
domain = entity.domain
|
||||||
|
|
||||||
# Fan Oscillating
|
data: dict[str, Any]
|
||||||
if instance != f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
|
||||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
|
||||||
|
|
||||||
service = fan.SERVICE_OSCILLATE
|
# Fan Oscillating
|
||||||
data: dict[str, Any] = {
|
if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||||
ATTR_ENTITY_ID: entity.entity_id,
|
service = fan.SERVICE_OSCILLATE
|
||||||
fan.ATTR_OSCILLATING: True,
|
data = {
|
||||||
}
|
ATTR_ENTITY_ID: entity.entity_id,
|
||||||
|
fan.ATTR_OSCILLATING: True,
|
||||||
|
}
|
||||||
|
elif instance == f"{valve.DOMAIN}.stop":
|
||||||
|
service = valve.SERVICE_STOP_VALVE
|
||||||
|
data = {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
domain, service, data, blocking=False, context=context
|
domain, service, data, blocking=False, context=context
|
||||||
@ -1375,6 +1434,17 @@ async def async_api_set_range(
|
|||||||
|
|
||||||
data[vacuum.ATTR_FAN_SPEED] = speed
|
data[vacuum.ATTR_FAN_SPEED] = speed
|
||||||
|
|
||||||
|
# Valve Position
|
||||||
|
elif instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
|
||||||
|
range_value = int(range_value)
|
||||||
|
if supported & valve.ValveEntityFeature.CLOSE and range_value == 0:
|
||||||
|
service = valve.SERVICE_CLOSE_VALVE
|
||||||
|
elif supported & valve.ValveEntityFeature.OPEN and range_value == 100:
|
||||||
|
service = valve.SERVICE_OPEN_VALVE
|
||||||
|
else:
|
||||||
|
service = valve.SERVICE_SET_VALVE_POSITION
|
||||||
|
data[valve.ATTR_POSITION] = range_value
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||||
|
|
||||||
@ -1520,6 +1590,21 @@ async def async_api_adjust_range(
|
|||||||
)
|
)
|
||||||
data[vacuum.ATTR_FAN_SPEED] = response_value = speed
|
data[vacuum.ATTR_FAN_SPEED] = response_value = speed
|
||||||
|
|
||||||
|
# Valve Position
|
||||||
|
elif instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
|
||||||
|
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
|
||||||
|
service = valve.SERVICE_SET_VALVE_POSITION
|
||||||
|
if not (current := entity.attributes.get(valve.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))
|
||||||
|
if position == 100:
|
||||||
|
service = valve.SERVICE_OPEN_VALVE
|
||||||
|
elif position == 0:
|
||||||
|
service = valve.SERVICE_CLOSE_VALVE
|
||||||
|
else:
|
||||||
|
data[valve.ATTR_POSITION] = position
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import logging
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
DOMAIN = "amberelectric"
|
DOMAIN = "amberelectric"
|
||||||
CONF_API_TOKEN = "api_token"
|
|
||||||
CONF_SITE_NAME = "site_name"
|
CONF_SITE_NAME = "site_name"
|
||||||
CONF_SITE_ID = "site_id"
|
CONF_SITE_ID = "site_id"
|
||||||
CONF_SITE_NMI = "site_nmi"
|
CONF_SITE_NMI = "site_nmi"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Config flow for Ambiclimate."""
|
"""Config flow for Ambiclimate."""
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import ambiclimate
|
import ambiclimate
|
||||||
@ -7,7 +8,8 @@ import ambiclimate
|
|||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.network import get_url
|
from homeassistant.helpers.network import get_url
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
@ -26,7 +28,9 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def register_flow_implementation(hass, client_id, client_secret):
|
def register_flow_implementation(
|
||||||
|
hass: HomeAssistant, client_id: str, client_secret: str
|
||||||
|
) -> None:
|
||||||
"""Register a ambiclimate implementation.
|
"""Register a ambiclimate implementation.
|
||||||
|
|
||||||
client_id: Client id.
|
client_id: Client id.
|
||||||
@ -50,7 +54,9 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._registered_view = False
|
self._registered_view = False
|
||||||
self._oauth = None
|
self._oauth = None
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
"""Handle external yaml configuration."""
|
"""Handle external yaml configuration."""
|
||||||
self._async_abort_entries_match()
|
self._async_abort_entries_match()
|
||||||
|
|
||||||
@ -62,7 +68,9 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
return await self.async_step_auth()
|
return await self.async_step_auth()
|
||||||
|
|
||||||
async def async_step_auth(self, user_input=None):
|
async def async_step_auth(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
"""Handle a flow start."""
|
"""Handle a flow start."""
|
||||||
self._async_abort_entries_match()
|
self._async_abort_entries_match()
|
||||||
|
|
||||||
@ -83,7 +91,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_code(self, code=None):
|
async def async_step_code(self, code: str | None = None) -> FlowResult:
|
||||||
"""Received code for authentication."""
|
"""Received code for authentication."""
|
||||||
self._async_abort_entries_match()
|
self._async_abort_entries_match()
|
||||||
|
|
||||||
@ -95,7 +103,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
return self.async_create_entry(title="Ambiclimate", data=config)
|
return self.async_create_entry(title="Ambiclimate", data=config)
|
||||||
|
|
||||||
async def _get_token_info(self, code):
|
async def _get_token_info(self, code: str | None) -> dict[str, Any] | None:
|
||||||
oauth = self._generate_oauth()
|
oauth = self._generate_oauth()
|
||||||
try:
|
try:
|
||||||
token_info = await oauth.get_access_token(code)
|
token_info = await oauth.get_access_token(code)
|
||||||
@ -103,16 +111,16 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.exception("Failed to get access token")
|
_LOGGER.exception("Failed to get access token")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY)
|
store = Store[dict[str, Any]](self.hass, STORAGE_VERSION, STORAGE_KEY)
|
||||||
await store.async_save(token_info)
|
await store.async_save(token_info)
|
||||||
|
|
||||||
return token_info
|
return token_info
|
||||||
|
|
||||||
def _generate_view(self):
|
def _generate_view(self) -> None:
|
||||||
self.hass.http.register_view(AmbiclimateAuthCallbackView())
|
self.hass.http.register_view(AmbiclimateAuthCallbackView())
|
||||||
self._registered_view = True
|
self._registered_view = True
|
||||||
|
|
||||||
def _generate_oauth(self):
|
def _generate_oauth(self) -> ambiclimate.AmbiclimateOAuth:
|
||||||
config = self.hass.data[DATA_AMBICLIMATE_IMPL]
|
config = self.hass.data[DATA_AMBICLIMATE_IMPL]
|
||||||
clientsession = async_get_clientsession(self.hass)
|
clientsession = async_get_clientsession(self.hass)
|
||||||
callback_url = self._cb_url()
|
callback_url = self._cb_url()
|
||||||
|
@ -63,14 +63,14 @@ TYPE_RELAY8 = "relay8"
|
|||||||
TYPE_RELAY9 = "relay9"
|
TYPE_RELAY9 = "relay9"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AmbientBinarySensorDescriptionMixin:
|
class AmbientBinarySensorDescriptionMixin:
|
||||||
"""Define an entity description mixin for binary sensors."""
|
"""Define an entity description mixin for binary sensors."""
|
||||||
|
|
||||||
on_state: Literal[0, 1]
|
on_state: Literal[0, 1]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AmbientBinarySensorDescription(
|
class AmbientBinarySensorDescription(
|
||||||
BinarySensorEntityDescription, AmbientBinarySensorDescriptionMixin
|
BinarySensorEntityDescription, AmbientBinarySensorDescriptionMixin
|
||||||
):
|
):
|
||||||
|
@ -35,7 +35,7 @@ if TYPE_CHECKING:
|
|||||||
from . import AmcrestDevice
|
from . import AmcrestDevice
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AmcrestSensorEntityDescription(BinarySensorEntityDescription):
|
class AmcrestSensorEntityDescription(BinarySensorEntityDescription):
|
||||||
"""Describe Amcrest sensor entity."""
|
"""Describe Amcrest sensor entity."""
|
||||||
|
|
||||||
|
@ -23,14 +23,14 @@ from .coordinator import AndroidIPCamDataUpdateCoordinator
|
|||||||
from .entity import AndroidIPCamBaseEntity
|
from .entity import AndroidIPCamBaseEntity
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AndroidIPWebcamSensorEntityDescriptionMixin:
|
class AndroidIPWebcamSensorEntityDescriptionMixin:
|
||||||
"""Mixin for required keys."""
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
value_fn: Callable[[PyDroidIPCam], StateType]
|
value_fn: Callable[[PyDroidIPCam], StateType]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AndroidIPWebcamSensorEntityDescription(
|
class AndroidIPWebcamSensorEntityDescription(
|
||||||
SensorEntityDescription, AndroidIPWebcamSensorEntityDescriptionMixin
|
SensorEntityDescription, AndroidIPWebcamSensorEntityDescriptionMixin
|
||||||
):
|
):
|
||||||
|
@ -18,7 +18,7 @@ from .coordinator import AndroidIPCamDataUpdateCoordinator
|
|||||||
from .entity import AndroidIPCamBaseEntity
|
from .entity import AndroidIPCamBaseEntity
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AndroidIPWebcamSwitchEntityDescriptionMixin:
|
class AndroidIPWebcamSwitchEntityDescriptionMixin:
|
||||||
"""Mixin for required keys."""
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ class AndroidIPWebcamSwitchEntityDescriptionMixin:
|
|||||||
off_func: Callable[[PyDroidIPCam], Coroutine[Any, Any, bool]]
|
off_func: Callable[[PyDroidIPCam], Coroutine[Any, Any, bool]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AndroidIPWebcamSwitchEntityDescription(
|
class AndroidIPWebcamSwitchEntityDescription(
|
||||||
SwitchEntityDescription, AndroidIPWebcamSwitchEntityDescriptionMixin
|
SwitchEntityDescription, AndroidIPWebcamSwitchEntityDescriptionMixin
|
||||||
):
|
):
|
||||||
|
@ -160,7 +160,7 @@ def adb_decorator(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def _adb_decorator(
|
def _adb_decorator(
|
||||||
func: _FuncType[_ADBDeviceT, _P, _R]
|
func: _FuncType[_ADBDeviceT, _P, _R],
|
||||||
) -> _ReturnFuncType[_ADBDeviceT, _P, _R]:
|
) -> _ReturnFuncType[_ADBDeviceT, _P, _R]:
|
||||||
"""Wrap the provided ADB method and catch exceptions."""
|
"""Wrap the provided ADB method and catch exceptions."""
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ from androidtvremote2 import (
|
|||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform
|
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@ -69,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def on_hass_stop(event) -> None:
|
def on_hass_stop(event: Event) -> None:
|
||||||
"""Stop push updates when hass stops."""
|
"""Stop push updates when hass stops."""
|
||||||
api.disconnect()
|
api.disconnect()
|
||||||
|
|
||||||
|
@ -23,14 +23,14 @@ from .entity import AnovaDescriptionEntity
|
|||||||
from .models import AnovaData
|
from .models import AnovaData
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AnovaSensorEntityDescriptionMixin:
|
class AnovaSensorEntityDescriptionMixin:
|
||||||
"""Describes the mixin variables for anova sensors."""
|
"""Describes the mixin variables for anova sensors."""
|
||||||
|
|
||||||
value_fn: Callable[[APCUpdateSensor], float | int | str]
|
value_fn: Callable[[APCUpdateSensor], float | int | str]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AnovaSensorEntityDescription(
|
class AnovaSensorEntityDescription(
|
||||||
SensorEntityDescription, AnovaSensorEntityDescriptionMixin
|
SensorEntityDescription, AnovaSensorEntityDescriptionMixin
|
||||||
):
|
):
|
||||||
|
@ -10,18 +10,12 @@ from anthemav.device_error import DeviceError
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow
|
from homeassistant.config_entries import ConfigFlow
|
||||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODEL, CONF_PORT
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
|
|
||||||
from .const import (
|
from .const import DEFAULT_NAME, DEFAULT_PORT, DEVICE_TIMEOUT_SECONDS, DOMAIN
|
||||||
CONF_MODEL,
|
|
||||||
DEFAULT_NAME,
|
|
||||||
DEFAULT_PORT,
|
|
||||||
DEVICE_TIMEOUT_SECONDS,
|
|
||||||
DOMAIN,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Constants for the Anthem A/V Receivers integration."""
|
"""Constants for the Anthem A/V Receivers integration."""
|
||||||
ANTHEMAV_UPDATE_SIGNAL = "anthemav_update"
|
ANTHEMAV_UPDATE_SIGNAL = "anthemav_update"
|
||||||
CONF_MODEL = "model"
|
|
||||||
DEFAULT_NAME = "Anthem AV"
|
DEFAULT_NAME = "Anthem AV"
|
||||||
DEFAULT_PORT = 14999
|
DEFAULT_PORT = 14999
|
||||||
DOMAIN = "anthemav"
|
DOMAIN = "anthemav"
|
||||||
|
@ -13,13 +13,13 @@ from homeassistant.components.media_player import (
|
|||||||
MediaPlayerState,
|
MediaPlayerState,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_MAC
|
from homeassistant.const import CONF_MAC, CONF_MODEL
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import ANTHEMAV_UPDATE_SIGNAL, CONF_MODEL, DOMAIN, MANUFACTURER
|
from .const import ANTHEMAV_UPDATE_SIGNAL, DOMAIN, MANUFACTURER
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
73
homeassistant/components/aosmith/__init__.py
Normal file
73
homeassistant/components/aosmith/__init__.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"""The A. O. Smith integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from py_aosmith import AOSmithAPIClient
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import aiohttp_client, device_registry as dr
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AOSmithEnergyCoordinator, AOSmithStatusCoordinator
|
||||||
|
|
||||||
|
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.WATER_HEATER]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AOSmithData:
|
||||||
|
"""Data for the A. O. Smith integration."""
|
||||||
|
|
||||||
|
client: AOSmithAPIClient
|
||||||
|
status_coordinator: AOSmithStatusCoordinator
|
||||||
|
energy_coordinator: AOSmithEnergyCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up A. O. Smith from a config entry."""
|
||||||
|
email = entry.data[CONF_EMAIL]
|
||||||
|
password = entry.data[CONF_PASSWORD]
|
||||||
|
|
||||||
|
session = aiohttp_client.async_get_clientsession(hass)
|
||||||
|
client = AOSmithAPIClient(email, password, session)
|
||||||
|
|
||||||
|
status_coordinator = AOSmithStatusCoordinator(hass, client)
|
||||||
|
await status_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
for junction_id, status_data in status_coordinator.data.items():
|
||||||
|
device_registry.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
identifiers={(DOMAIN, junction_id)},
|
||||||
|
manufacturer="A. O. Smith",
|
||||||
|
name=status_data.get("name"),
|
||||||
|
model=status_data.get("model"),
|
||||||
|
serial_number=status_data.get("serial"),
|
||||||
|
suggested_area=status_data.get("install", {}).get("location"),
|
||||||
|
sw_version=status_data.get("data", {}).get("firmwareVersion"),
|
||||||
|
)
|
||||||
|
|
||||||
|
energy_coordinator = AOSmithEnergyCoordinator(
|
||||||
|
hass, client, list(status_coordinator.data)
|
||||||
|
)
|
||||||
|
await energy_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AOSmithData(
|
||||||
|
client,
|
||||||
|
status_coordinator,
|
||||||
|
energy_coordinator,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
107
homeassistant/components/aosmith/config_flow.py
Normal file
107
homeassistant/components/aosmith/config_flow.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
"""Config flow for A. O. Smith integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from py_aosmith import AOSmithAPIClient, AOSmithInvalidCredentialsException
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for A. O. Smith."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
_reauth_email: str | None = None
|
||||||
|
|
||||||
|
async def _async_validate_credentials(
|
||||||
|
self, email: str, password: str
|
||||||
|
) -> str | None:
|
||||||
|
"""Validate the credentials. Return an error string, or None if successful."""
|
||||||
|
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
|
client = AOSmithAPIClient(email, password, session)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await client.get_devices()
|
||||||
|
except AOSmithInvalidCredentialsException:
|
||||||
|
return "invalid_auth"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input is not None:
|
||||||
|
unique_id = user_input[CONF_EMAIL].lower()
|
||||||
|
await self.async_set_unique_id(unique_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
error = await self._async_validate_credentials(
|
||||||
|
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||||
|
)
|
||||||
|
if error is None:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_EMAIL], data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
errors["base"] = error
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_EMAIL): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
|
||||||
|
"""Perform reauth if the user credentials have changed."""
|
||||||
|
self._reauth_email = entry_data[CONF_EMAIL]
|
||||||
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle user's reauth credentials."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input is not None and self._reauth_email is not None:
|
||||||
|
email = self._reauth_email
|
||||||
|
password = user_input[CONF_PASSWORD]
|
||||||
|
entry_id = self.context["entry_id"]
|
||||||
|
|
||||||
|
if entry := self.hass.config_entries.async_get_entry(entry_id):
|
||||||
|
error = await self._async_validate_credentials(email, password)
|
||||||
|
if error is None:
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
entry,
|
||||||
|
data=entry.data | user_input,
|
||||||
|
)
|
||||||
|
await self.hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
return self.async_abort(reason="reauth_successful")
|
||||||
|
errors["base"] = error
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reauth_confirm",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
|
||||||
|
description_placeholders={CONF_EMAIL: self._reauth_email},
|
||||||
|
errors=errors,
|
||||||
|
)
|
25
homeassistant/components/aosmith/const.py
Normal file
25
homeassistant/components/aosmith/const.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""Constants for the A. O. Smith integration."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
DOMAIN = "aosmith"
|
||||||
|
|
||||||
|
AOSMITH_MODE_ELECTRIC = "ELECTRIC"
|
||||||
|
AOSMITH_MODE_HEAT_PUMP = "HEAT_PUMP"
|
||||||
|
AOSMITH_MODE_HYBRID = "HYBRID"
|
||||||
|
AOSMITH_MODE_VACATION = "VACATION"
|
||||||
|
|
||||||
|
# Update interval to be used for normal background updates.
|
||||||
|
REGULAR_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
|
# Update interval to be used while a mode or setpoint change is in progress.
|
||||||
|
FAST_INTERVAL = timedelta(seconds=1)
|
||||||
|
|
||||||
|
# Update interval to be used for energy usage data.
|
||||||
|
ENERGY_USAGE_INTERVAL = timedelta(minutes=10)
|
||||||
|
|
||||||
|
HOT_WATER_STATUS_MAP = {
|
||||||
|
"LOW": "low",
|
||||||
|
"MEDIUM": "medium",
|
||||||
|
"HIGH": "high",
|
||||||
|
}
|
83
homeassistant/components/aosmith/coordinator.py
Normal file
83
homeassistant/components/aosmith/coordinator.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
"""The data update coordinator for the A. O. Smith integration."""
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from py_aosmith import (
|
||||||
|
AOSmithAPIClient,
|
||||||
|
AOSmithInvalidCredentialsException,
|
||||||
|
AOSmithUnknownException,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DOMAIN, ENERGY_USAGE_INTERVAL, FAST_INTERVAL, REGULAR_INTERVAL
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AOSmithStatusCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||||
|
"""Coordinator for device status, updating with a frequent interval."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, client: AOSmithAPIClient) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=REGULAR_INTERVAL)
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
||||||
|
"""Fetch latest data from the device status endpoint."""
|
||||||
|
try:
|
||||||
|
devices = await self.client.get_devices()
|
||||||
|
except AOSmithInvalidCredentialsException as err:
|
||||||
|
raise ConfigEntryAuthFailed from err
|
||||||
|
except AOSmithUnknownException as err:
|
||||||
|
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||||
|
|
||||||
|
mode_pending = any(
|
||||||
|
device.get("data", {}).get("modePending") for device in devices
|
||||||
|
)
|
||||||
|
setpoint_pending = any(
|
||||||
|
device.get("data", {}).get("temperatureSetpointPending")
|
||||||
|
for device in devices
|
||||||
|
)
|
||||||
|
|
||||||
|
if mode_pending or setpoint_pending:
|
||||||
|
self.update_interval = FAST_INTERVAL
|
||||||
|
else:
|
||||||
|
self.update_interval = REGULAR_INTERVAL
|
||||||
|
|
||||||
|
return {device.get("junctionId"): device for device in devices}
|
||||||
|
|
||||||
|
|
||||||
|
class AOSmithEnergyCoordinator(DataUpdateCoordinator[dict[str, float]]):
|
||||||
|
"""Coordinator for energy usage data, updating with a slower interval."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
client: AOSmithAPIClient,
|
||||||
|
junction_ids: list[str],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass, _LOGGER, name=DOMAIN, update_interval=ENERGY_USAGE_INTERVAL
|
||||||
|
)
|
||||||
|
self.client = client
|
||||||
|
self.junction_ids = junction_ids
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, float]:
|
||||||
|
"""Fetch latest data from the energy usage endpoint."""
|
||||||
|
energy_usage_by_junction_id: dict[str, float] = {}
|
||||||
|
|
||||||
|
for junction_id in self.junction_ids:
|
||||||
|
try:
|
||||||
|
energy_usage = await self.client.get_energy_use_data(junction_id)
|
||||||
|
except AOSmithInvalidCredentialsException as err:
|
||||||
|
raise ConfigEntryAuthFailed from err
|
||||||
|
except AOSmithUnknownException as err:
|
||||||
|
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||||
|
|
||||||
|
energy_usage_by_junction_id[junction_id] = energy_usage.get("lifetimeKwh")
|
||||||
|
|
||||||
|
return energy_usage_by_junction_id
|
62
homeassistant/components/aosmith/entity.py
Normal file
62
homeassistant/components/aosmith/entity.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""The base entity for the A. O. Smith integration."""
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from py_aosmith import AOSmithAPIClient
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AOSmithEnergyCoordinator, AOSmithStatusCoordinator
|
||||||
|
|
||||||
|
_AOSmithCoordinatorT = TypeVar(
|
||||||
|
"_AOSmithCoordinatorT", bound=AOSmithStatusCoordinator | AOSmithEnergyCoordinator
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AOSmithEntity(CoordinatorEntity[_AOSmithCoordinatorT]):
|
||||||
|
"""Base entity for A. O. Smith."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(self, coordinator: _AOSmithCoordinatorT, junction_id: str) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.junction_id = junction_id
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, junction_id)},
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self) -> AOSmithAPIClient:
|
||||||
|
"""Shortcut to get the API client."""
|
||||||
|
return self.coordinator.client
|
||||||
|
|
||||||
|
|
||||||
|
class AOSmithStatusEntity(AOSmithEntity[AOSmithStatusCoordinator]):
|
||||||
|
"""Base entity for entities that use data from the status coordinator."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self):
|
||||||
|
"""Shortcut to get the device status from the coordinator data."""
|
||||||
|
return self.coordinator.data.get(self.junction_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_data(self):
|
||||||
|
"""Shortcut to get the device data within the device status."""
|
||||||
|
device = self.device
|
||||||
|
return None if device is None else device.get("data", {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return super().available and self.device_data.get("isOnline") is True
|
||||||
|
|
||||||
|
|
||||||
|
class AOSmithEnergyEntity(AOSmithEntity[AOSmithEnergyCoordinator]):
|
||||||
|
"""Base entity for entities that use data from the energy coordinator."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def energy_usage(self) -> float | None:
|
||||||
|
"""Shortcut to get the energy usage from the coordinator data."""
|
||||||
|
return self.coordinator.data.get(self.junction_id)
|
9
homeassistant/components/aosmith/manifest.json
Normal file
9
homeassistant/components/aosmith/manifest.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"domain": "aosmith",
|
||||||
|
"name": "A. O. Smith",
|
||||||
|
"codeowners": ["@bdr99"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"requirements": ["py-aosmith==1.0.1"]
|
||||||
|
}
|
106
homeassistant/components/aosmith/sensor.py
Normal file
106
homeassistant/components/aosmith/sensor.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
"""The sensor platform for the A. O. Smith integration."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import UnitOfEnergy
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AOSmithData
|
||||||
|
from .const import DOMAIN, HOT_WATER_STATUS_MAP
|
||||||
|
from .coordinator import AOSmithEnergyCoordinator, AOSmithStatusCoordinator
|
||||||
|
from .entity import AOSmithEnergyEntity, AOSmithStatusEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AOSmithStatusSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Entity description class for sensors using data from the status coordinator."""
|
||||||
|
|
||||||
|
value_fn: Callable[[dict[str, Any]], str | int | None]
|
||||||
|
|
||||||
|
|
||||||
|
STATUS_ENTITY_DESCRIPTIONS: tuple[AOSmithStatusSensorEntityDescription, ...] = (
|
||||||
|
AOSmithStatusSensorEntityDescription(
|
||||||
|
key="hot_water_availability",
|
||||||
|
translation_key="hot_water_availability",
|
||||||
|
icon="mdi:water-thermometer",
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=["low", "medium", "high"],
|
||||||
|
value_fn=lambda device: HOT_WATER_STATUS_MAP.get(
|
||||||
|
device.get("data", {}).get("hotWaterStatus")
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
"""Set up A. O. Smith sensor platform."""
|
||||||
|
data: AOSmithData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
AOSmithStatusSensorEntity(data.status_coordinator, description, junction_id)
|
||||||
|
for description in STATUS_ENTITY_DESCRIPTIONS
|
||||||
|
for junction_id in data.status_coordinator.data
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
AOSmithEnergySensorEntity(data.energy_coordinator, junction_id)
|
||||||
|
for junction_id in data.status_coordinator.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AOSmithStatusSensorEntity(AOSmithStatusEntity, SensorEntity):
|
||||||
|
"""Class for sensor entities that use data from the status coordinator."""
|
||||||
|
|
||||||
|
entity_description: AOSmithStatusSensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AOSmithStatusCoordinator,
|
||||||
|
description: AOSmithStatusSensorEntityDescription,
|
||||||
|
junction_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator, junction_id)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{description.key}_{junction_id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> str | int | None:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.entity_description.value_fn(self.device)
|
||||||
|
|
||||||
|
|
||||||
|
class AOSmithEnergySensorEntity(AOSmithEnergyEntity, SensorEntity):
|
||||||
|
"""Class for the energy sensor entity."""
|
||||||
|
|
||||||
|
_attr_translation_key = "energy_usage"
|
||||||
|
_attr_device_class = SensorDeviceClass.ENERGY
|
||||||
|
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||||
|
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||||
|
_attr_suggested_display_precision = 1
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AOSmithEnergyCoordinator,
|
||||||
|
junction_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator, junction_id)
|
||||||
|
self._attr_unique_id = f"energy_usage_{junction_id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float | None:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.energy_usage
|
43
homeassistant/components/aosmith/strings.json
Normal file
43
homeassistant/components/aosmith/strings.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"email": "[%key:common::config_flow::data::email%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"description": "Please enter your A. O. Smith credentials."
|
||||||
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"description": "Please update your password for {email}",
|
||||||
|
"title": "[%key:common::config_flow::title::reauth%]",
|
||||||
|
"data": {
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"hot_water_availability": {
|
||||||
|
"name": "Hot water availability",
|
||||||
|
"state": {
|
||||||
|
"low": "Low",
|
||||||
|
"medium": "Medium",
|
||||||
|
"high": "High"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"energy_usage": {
|
||||||
|
"name": "Energy usage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
151
homeassistant/components/aosmith/water_heater.py
Normal file
151
homeassistant/components/aosmith/water_heater.py
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
"""The water heater platform for the A. O. Smith integration."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.water_heater import (
|
||||||
|
STATE_ECO,
|
||||||
|
STATE_ELECTRIC,
|
||||||
|
STATE_HEAT_PUMP,
|
||||||
|
STATE_OFF,
|
||||||
|
WaterHeaterEntity,
|
||||||
|
WaterHeaterEntityFeature,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import UnitOfTemperature
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AOSmithData
|
||||||
|
from .const import (
|
||||||
|
AOSMITH_MODE_ELECTRIC,
|
||||||
|
AOSMITH_MODE_HEAT_PUMP,
|
||||||
|
AOSMITH_MODE_HYBRID,
|
||||||
|
AOSMITH_MODE_VACATION,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from .coordinator import AOSmithStatusCoordinator
|
||||||
|
from .entity import AOSmithStatusEntity
|
||||||
|
|
||||||
|
MODE_HA_TO_AOSMITH = {
|
||||||
|
STATE_OFF: AOSMITH_MODE_VACATION,
|
||||||
|
STATE_ECO: AOSMITH_MODE_HYBRID,
|
||||||
|
STATE_ELECTRIC: AOSMITH_MODE_ELECTRIC,
|
||||||
|
STATE_HEAT_PUMP: AOSMITH_MODE_HEAT_PUMP,
|
||||||
|
}
|
||||||
|
MODE_AOSMITH_TO_HA = {
|
||||||
|
AOSMITH_MODE_ELECTRIC: STATE_ELECTRIC,
|
||||||
|
AOSMITH_MODE_HEAT_PUMP: STATE_HEAT_PUMP,
|
||||||
|
AOSMITH_MODE_HYBRID: STATE_ECO,
|
||||||
|
AOSMITH_MODE_VACATION: STATE_OFF,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Operation mode to use when exiting away mode
|
||||||
|
DEFAULT_OPERATION_MODE = AOSMITH_MODE_HYBRID
|
||||||
|
|
||||||
|
DEFAULT_SUPPORT_FLAGS = (
|
||||||
|
WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||||
|
| WaterHeaterEntityFeature.OPERATION_MODE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
"""Set up A. O. Smith water heater platform."""
|
||||||
|
data: AOSmithData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
AOSmithWaterHeaterEntity(data.status_coordinator, junction_id)
|
||||||
|
for junction_id in data.status_coordinator.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AOSmithWaterHeaterEntity(AOSmithStatusEntity, WaterHeaterEntity):
|
||||||
|
"""The water heater entity for the A. O. Smith integration."""
|
||||||
|
|
||||||
|
_attr_name = None
|
||||||
|
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
|
||||||
|
_attr_min_temp = 95
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AOSmithStatusCoordinator,
|
||||||
|
junction_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator, junction_id)
|
||||||
|
self._attr_unique_id = junction_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def operation_list(self) -> list[str]:
|
||||||
|
"""Return the list of supported operation modes."""
|
||||||
|
op_modes = []
|
||||||
|
for mode_dict in self.device_data.get("modes", []):
|
||||||
|
mode_name = mode_dict.get("mode")
|
||||||
|
ha_mode = MODE_AOSMITH_TO_HA.get(mode_name)
|
||||||
|
|
||||||
|
# Filtering out STATE_OFF since it is handled by away mode
|
||||||
|
if ha_mode is not None and ha_mode != STATE_OFF:
|
||||||
|
op_modes.append(ha_mode)
|
||||||
|
|
||||||
|
return op_modes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self) -> WaterHeaterEntityFeature:
|
||||||
|
"""Return the list of supported features."""
|
||||||
|
supports_vacation_mode = any(
|
||||||
|
mode_dict.get("mode") == AOSMITH_MODE_VACATION
|
||||||
|
for mode_dict in self.device_data.get("modes", [])
|
||||||
|
)
|
||||||
|
|
||||||
|
if supports_vacation_mode:
|
||||||
|
return DEFAULT_SUPPORT_FLAGS | WaterHeaterEntityFeature.AWAY_MODE
|
||||||
|
|
||||||
|
return DEFAULT_SUPPORT_FLAGS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature(self) -> float | None:
|
||||||
|
"""Return the temperature we try to reach."""
|
||||||
|
return self.device_data.get("temperatureSetpoint")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_temp(self) -> float:
|
||||||
|
"""Return the maximum temperature."""
|
||||||
|
return self.device_data.get("temperatureSetpointMaximum")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_operation(self) -> str:
|
||||||
|
"""Return the current operation mode."""
|
||||||
|
return MODE_AOSMITH_TO_HA.get(self.device_data.get("mode"), STATE_OFF)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_away_mode_on(self):
|
||||||
|
"""Return True if away mode is on."""
|
||||||
|
return self.device_data.get("mode") == AOSMITH_MODE_VACATION
|
||||||
|
|
||||||
|
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||||
|
"""Set new target operation mode."""
|
||||||
|
aosmith_mode = MODE_HA_TO_AOSMITH.get(operation_mode)
|
||||||
|
if aosmith_mode is not None:
|
||||||
|
await self.client.update_mode(self.junction_id, aosmith_mode)
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
|
"""Set new target temperature."""
|
||||||
|
temperature = kwargs.get("temperature")
|
||||||
|
await self.client.update_setpoint(self.junction_id, temperature)
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_turn_away_mode_on(self) -> None:
|
||||||
|
"""Turn away mode on."""
|
||||||
|
await self.client.update_mode(self.junction_id, AOSMITH_MODE_VACATION)
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_turn_away_mode_off(self) -> None:
|
||||||
|
"""Turn away mode off."""
|
||||||
|
await self.client.update_mode(self.junction_id, DEFAULT_OPERATION_MODE)
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
@ -7,8 +7,9 @@ from datetime import timedelta
|
|||||||
import logging
|
import logging
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from apcaccess import status
|
import aioapcaccess
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.debounce import Debouncer
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
@ -32,6 +33,8 @@ class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]):
|
|||||||
updates from the server.
|
updates from the server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
config_entry: ConfigEntry
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, host: str, port: int) -> None:
|
def __init__(self, hass: HomeAssistant, host: str, port: int) -> None:
|
||||||
"""Initialize the data object."""
|
"""Initialize the data object."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@ -70,13 +73,10 @@ class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]):
|
|||||||
return self.data.get("SERIALNO")
|
return self.data.get("SERIALNO")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_info(self) -> DeviceInfo | None:
|
def device_info(self) -> DeviceInfo:
|
||||||
"""Return the DeviceInfo of this APC UPS, if serial number is available."""
|
"""Return the DeviceInfo of this APC UPS, if serial number is available."""
|
||||||
if not self.ups_serial_no:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
identifiers={(DOMAIN, self.ups_serial_no)},
|
identifiers={(DOMAIN, self.ups_serial_no or self.config_entry.entry_id)},
|
||||||
model=self.ups_model,
|
model=self.ups_model,
|
||||||
manufacturer="APC",
|
manufacturer="APC",
|
||||||
name=self.ups_name if self.ups_name else "APC UPS",
|
name=self.ups_name if self.ups_name else "APC UPS",
|
||||||
@ -90,13 +90,8 @@ class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]):
|
|||||||
Note that the result dict uses upper case for each resource, where our
|
Note that the result dict uses upper case for each resource, where our
|
||||||
integration uses lower cases as keys internally.
|
integration uses lower cases as keys internally.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async with asyncio.timeout(10):
|
async with asyncio.timeout(10):
|
||||||
try:
|
try:
|
||||||
raw = await self.hass.async_add_executor_job(
|
return await aioapcaccess.request_status(self._host, self._port)
|
||||||
status.get, self._host, self._port
|
except (OSError, asyncio.IncompleteReadError) as error:
|
||||||
)
|
|
||||||
result: OrderedDict[str, str] = status.parse(raw)
|
|
||||||
return result
|
|
||||||
except OSError as error:
|
|
||||||
raise UpdateFailed(error) from error
|
raise UpdateFailed(error) from error
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["apcaccess"],
|
"loggers": ["apcaccess"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["apcaccess==0.0.13"]
|
"requirements": ["aioapcaccess==0.4.2"]
|
||||||
}
|
}
|
||||||
|
1
homeassistant/components/appalachianpower/__init__.py
Normal file
1
homeassistant/components/appalachianpower/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Virtual integration: Appalachian Power."""
|
6
homeassistant/components/appalachianpower/manifest.json
Normal file
6
homeassistant/components/appalachianpower/manifest.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"domain": "appalachianpower",
|
||||||
|
"name": "Appalachian Power",
|
||||||
|
"integration_type": "virtual",
|
||||||
|
"supported_by": "opower"
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import apprise
|
import apprise
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -61,11 +62,11 @@ def get_service(
|
|||||||
class AppriseNotificationService(BaseNotificationService):
|
class AppriseNotificationService(BaseNotificationService):
|
||||||
"""Implement the notification service for Apprise."""
|
"""Implement the notification service for Apprise."""
|
||||||
|
|
||||||
def __init__(self, a_obj):
|
def __init__(self, a_obj: apprise.Apprise) -> None:
|
||||||
"""Initialize the service."""
|
"""Initialize the service."""
|
||||||
self.apprise = a_obj
|
self.apprise = a_obj
|
||||||
|
|
||||||
def send_message(self, message="", **kwargs):
|
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||||
"""Send a message to a specified target.
|
"""Send a message to a specified target.
|
||||||
|
|
||||||
If no target/tags are specified, then services are notified as is
|
If no target/tags are specified, then services are notified as is
|
||||||
|
@ -26,7 +26,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|||||||
from . import DOMAIN, UPDATE_TOPIC, AquaLogicProcessor
|
from . import DOMAIN, UPDATE_TOPIC, AquaLogicProcessor
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AquaLogicSensorEntityDescription(SensorEntityDescription):
|
class AquaLogicSensorEntityDescription(SensorEntityDescription):
|
||||||
"""Describes AquaLogic sensor entity."""
|
"""Describes AquaLogic sensor entity."""
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AranetSensorEntityDescription(SensorEntityDescription):
|
class AranetSensorEntityDescription(SensorEntityDescription):
|
||||||
"""Class to describe an Aranet sensor entity."""
|
"""Class to describe an Aranet sensor entity."""
|
||||||
|
|
||||||
|
@ -20,14 +20,14 @@ from .coordinator import AsekoDataUpdateCoordinator
|
|||||||
from .entity import AsekoEntity
|
from .entity import AsekoEntity
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AsekoBinarySensorDescriptionMixin:
|
class AsekoBinarySensorDescriptionMixin:
|
||||||
"""Mixin for required keys."""
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
value_fn: Callable[[Unit], bool]
|
value_fn: Callable[[Unit], bool]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AsekoBinarySensorEntityDescription(
|
class AsekoBinarySensorEntityDescription(
|
||||||
BinarySensorEntityDescription, AsekoBinarySensorDescriptionMixin
|
BinarySensorEntityDescription, AsekoBinarySensorDescriptionMixin
|
||||||
):
|
):
|
||||||
|
@ -31,6 +31,7 @@ from .pipeline import (
|
|||||||
async_get_pipeline,
|
async_get_pipeline,
|
||||||
async_get_pipelines,
|
async_get_pipelines,
|
||||||
async_setup_pipeline_store,
|
async_setup_pipeline_store,
|
||||||
|
async_update_pipeline,
|
||||||
)
|
)
|
||||||
from .websocket_api import async_register_websocket_api
|
from .websocket_api import async_register_websocket_api
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ __all__ = (
|
|||||||
"async_get_pipelines",
|
"async_get_pipelines",
|
||||||
"async_setup",
|
"async_setup",
|
||||||
"async_pipeline_from_audio_stream",
|
"async_pipeline_from_audio_stream",
|
||||||
|
"async_update_pipeline",
|
||||||
"AudioSettings",
|
"AudioSettings",
|
||||||
"Pipeline",
|
"Pipeline",
|
||||||
"PipelineEvent",
|
"PipelineEvent",
|
||||||
|
@ -43,6 +43,7 @@ from homeassistant.helpers.collection import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.singleton import singleton
|
from homeassistant.helpers.singleton import singleton
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
|
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||||
from homeassistant.util import (
|
from homeassistant.util import (
|
||||||
dt as dt_util,
|
dt as dt_util,
|
||||||
language as language_util,
|
language as language_util,
|
||||||
@ -115,6 +116,7 @@ async def _async_resolve_default_pipeline_settings(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
stt_engine_id: str | None,
|
stt_engine_id: str | None,
|
||||||
tts_engine_id: str | None,
|
tts_engine_id: str | None,
|
||||||
|
pipeline_name: str,
|
||||||
) -> dict[str, str | None]:
|
) -> dict[str, str | None]:
|
||||||
"""Resolve settings for a default pipeline.
|
"""Resolve settings for a default pipeline.
|
||||||
|
|
||||||
@ -123,7 +125,6 @@ async def _async_resolve_default_pipeline_settings(
|
|||||||
"""
|
"""
|
||||||
conversation_language = "en"
|
conversation_language = "en"
|
||||||
pipeline_language = "en"
|
pipeline_language = "en"
|
||||||
pipeline_name = "Home Assistant"
|
|
||||||
stt_engine = None
|
stt_engine = None
|
||||||
stt_language = None
|
stt_language = None
|
||||||
tts_engine = None
|
tts_engine = None
|
||||||
@ -195,9 +196,6 @@ async def _async_resolve_default_pipeline_settings(
|
|||||||
)
|
)
|
||||||
tts_engine_id = None
|
tts_engine_id = None
|
||||||
|
|
||||||
if stt_engine_id == "cloud" and tts_engine_id == "cloud":
|
|
||||||
pipeline_name = "Home Assistant Cloud"
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"conversation_engine": conversation.HOME_ASSISTANT_AGENT,
|
"conversation_engine": conversation.HOME_ASSISTANT_AGENT,
|
||||||
"conversation_language": conversation_language,
|
"conversation_language": conversation_language,
|
||||||
@ -221,12 +219,17 @@ async def _async_create_default_pipeline(
|
|||||||
The default pipeline will use the homeassistant conversation agent and the
|
The default pipeline will use the homeassistant conversation agent and the
|
||||||
default stt / tts engines.
|
default stt / tts engines.
|
||||||
"""
|
"""
|
||||||
pipeline_settings = await _async_resolve_default_pipeline_settings(hass, None, None)
|
pipeline_settings = await _async_resolve_default_pipeline_settings(
|
||||||
|
hass, stt_engine_id=None, tts_engine_id=None, pipeline_name="Home Assistant"
|
||||||
|
)
|
||||||
return await pipeline_store.async_create_item(pipeline_settings)
|
return await pipeline_store.async_create_item(pipeline_settings)
|
||||||
|
|
||||||
|
|
||||||
async def async_create_default_pipeline(
|
async def async_create_default_pipeline(
|
||||||
hass: HomeAssistant, stt_engine_id: str, tts_engine_id: str
|
hass: HomeAssistant,
|
||||||
|
stt_engine_id: str,
|
||||||
|
tts_engine_id: str,
|
||||||
|
pipeline_name: str,
|
||||||
) -> Pipeline | None:
|
) -> Pipeline | None:
|
||||||
"""Create a pipeline with default settings.
|
"""Create a pipeline with default settings.
|
||||||
|
|
||||||
@ -236,7 +239,7 @@ async def async_create_default_pipeline(
|
|||||||
pipeline_data: PipelineData = hass.data[DOMAIN]
|
pipeline_data: PipelineData = hass.data[DOMAIN]
|
||||||
pipeline_store = pipeline_data.pipeline_store
|
pipeline_store = pipeline_data.pipeline_store
|
||||||
pipeline_settings = await _async_resolve_default_pipeline_settings(
|
pipeline_settings = await _async_resolve_default_pipeline_settings(
|
||||||
hass, stt_engine_id, tts_engine_id
|
hass, stt_engine_id, tts_engine_id, pipeline_name=pipeline_name
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
pipeline_settings["stt_engine"] != stt_engine_id
|
pipeline_settings["stt_engine"] != stt_engine_id
|
||||||
@ -274,6 +277,48 @@ def async_get_pipelines(hass: HomeAssistant) -> Iterable[Pipeline]:
|
|||||||
return pipeline_data.pipeline_store.data.values()
|
return pipeline_data.pipeline_store.data.values()
|
||||||
|
|
||||||
|
|
||||||
|
async def async_update_pipeline(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
pipeline: Pipeline,
|
||||||
|
*,
|
||||||
|
conversation_engine: str | UndefinedType = UNDEFINED,
|
||||||
|
conversation_language: str | UndefinedType = UNDEFINED,
|
||||||
|
language: str | UndefinedType = UNDEFINED,
|
||||||
|
name: str | UndefinedType = UNDEFINED,
|
||||||
|
stt_engine: str | None | UndefinedType = UNDEFINED,
|
||||||
|
stt_language: str | None | UndefinedType = UNDEFINED,
|
||||||
|
tts_engine: str | None | UndefinedType = UNDEFINED,
|
||||||
|
tts_language: str | None | UndefinedType = UNDEFINED,
|
||||||
|
tts_voice: str | None | UndefinedType = UNDEFINED,
|
||||||
|
wake_word_entity: str | None | UndefinedType = UNDEFINED,
|
||||||
|
wake_word_id: str | None | UndefinedType = UNDEFINED,
|
||||||
|
) -> None:
|
||||||
|
"""Update a pipeline."""
|
||||||
|
pipeline_data: PipelineData = hass.data[DOMAIN]
|
||||||
|
|
||||||
|
updates: dict[str, Any] = pipeline.to_json()
|
||||||
|
updates.pop("id")
|
||||||
|
# Refactor this once we bump to Python 3.12
|
||||||
|
# and have https://peps.python.org/pep-0692/
|
||||||
|
for key, val in (
|
||||||
|
("conversation_engine", conversation_engine),
|
||||||
|
("conversation_language", conversation_language),
|
||||||
|
("language", language),
|
||||||
|
("name", name),
|
||||||
|
("stt_engine", stt_engine),
|
||||||
|
("stt_language", stt_language),
|
||||||
|
("tts_engine", tts_engine),
|
||||||
|
("tts_language", tts_language),
|
||||||
|
("tts_voice", tts_voice),
|
||||||
|
("wake_word_entity", wake_word_entity),
|
||||||
|
("wake_word_id", wake_word_id),
|
||||||
|
):
|
||||||
|
if val is not UNDEFINED:
|
||||||
|
updates[key] = val
|
||||||
|
|
||||||
|
await pipeline_data.pipeline_store.async_update_item(pipeline.id, updates)
|
||||||
|
|
||||||
|
|
||||||
class PipelineEventType(StrEnum):
|
class PipelineEventType(StrEnum):
|
||||||
"""Event types emitted during a pipeline run."""
|
"""Event types emitted during a pipeline run."""
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ from .const import (
|
|||||||
SENSORS_LOAD_AVG,
|
SENSORS_LOAD_AVG,
|
||||||
SENSORS_RATES,
|
SENSORS_RATES,
|
||||||
SENSORS_TEMPERATURES,
|
SENSORS_TEMPERATURES,
|
||||||
|
SENSORS_TEMPERATURES_LEGACY,
|
||||||
)
|
)
|
||||||
|
|
||||||
SENSORS_TYPE_BYTES = "sensors_bytes"
|
SENSORS_TYPE_BYTES = "sensors_bytes"
|
||||||
@ -277,7 +278,7 @@ class AsusWrtLegacyBridge(AsusWrtBridge):
|
|||||||
async def _get_available_temperature_sensors(self) -> list[str]:
|
async def _get_available_temperature_sensors(self) -> list[str]:
|
||||||
"""Check which temperature information is available on the router."""
|
"""Check which temperature information is available on the router."""
|
||||||
availability = await self._api.async_find_temperature_commands()
|
availability = await self._api.async_find_temperature_commands()
|
||||||
return [SENSORS_TEMPERATURES[i] for i in range(3) if availability[i]]
|
return [SENSORS_TEMPERATURES_LEGACY[i] for i in range(3) if availability[i]]
|
||||||
|
|
||||||
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_BYTES)
|
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_BYTES)
|
||||||
async def _get_bytes(self) -> Any:
|
async def _get_bytes(self) -> Any:
|
||||||
|
@ -30,4 +30,5 @@ SENSORS_BYTES = ["sensor_rx_bytes", "sensor_tx_bytes"]
|
|||||||
SENSORS_CONNECTED_DEVICE = ["sensor_connected_device"]
|
SENSORS_CONNECTED_DEVICE = ["sensor_connected_device"]
|
||||||
SENSORS_LOAD_AVG = ["sensor_load_avg1", "sensor_load_avg5", "sensor_load_avg15"]
|
SENSORS_LOAD_AVG = ["sensor_load_avg1", "sensor_load_avg5", "sensor_load_avg15"]
|
||||||
SENSORS_RATES = ["sensor_rx_rates", "sensor_tx_rates"]
|
SENSORS_RATES = ["sensor_rx_rates", "sensor_tx_rates"]
|
||||||
SENSORS_TEMPERATURES = ["2.4GHz", "5.0GHz", "CPU"]
|
SENSORS_TEMPERATURES_LEGACY = ["2.4GHz", "5.0GHz", "CPU"]
|
||||||
|
SENSORS_TEMPERATURES = [*SENSORS_TEMPERATURES_LEGACY, "5.0GHz_2", "6.0GHz"]
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioasuswrt", "asyncssh"],
|
"loggers": ["aioasuswrt", "asyncssh"],
|
||||||
"requirements": ["aioasuswrt==1.4.0", "pyasuswrt==0.1.20"]
|
"requirements": ["aioasuswrt==1.4.0", "pyasuswrt==0.1.21"]
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ from .const import (
|
|||||||
from .router import AsusWrtRouter
|
from .router import AsusWrtRouter
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class AsusWrtSensorEntityDescription(SensorEntityDescription):
|
class AsusWrtSensorEntityDescription(SensorEntityDescription):
|
||||||
"""A class that describes AsusWrt sensor entities."""
|
"""A class that describes AsusWrt sensor entities."""
|
||||||
|
|
||||||
@ -156,6 +156,26 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
|
|||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
suggested_display_precision=1,
|
suggested_display_precision=1,
|
||||||
),
|
),
|
||||||
|
AsusWrtSensorEntityDescription(
|
||||||
|
key=SENSORS_TEMPERATURES[3],
|
||||||
|
translation_key="5ghz_2_temperature",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
),
|
||||||
|
AsusWrtSensorEntityDescription(
|
||||||
|
key=SENSORS_TEMPERATURES[4],
|
||||||
|
translation_key="6ghz_temperature",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -82,6 +82,12 @@
|
|||||||
},
|
},
|
||||||
"cpu_temperature": {
|
"cpu_temperature": {
|
||||||
"name": "CPU Temperature"
|
"name": "CPU Temperature"
|
||||||
|
},
|
||||||
|
"5ghz_2_temperature": {
|
||||||
|
"name": "5GHz Temperature (Radio 2)"
|
||||||
|
},
|
||||||
|
"6ghz_temperature": {
|
||||||
|
"name": "6GHz Temperature"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user