Compare commits

...

120 Commits

Author SHA1 Message Date
Franck Nijhof 4f25e98999 Merge pull request #62690 from home-assistant/rc 2021-12-23 18:17:49 +01:00
Franck Nijhof d1a4e73ffd Bumped version to 2021.12.5 2021-12-23 17:24:05 +01:00
Marcel van der Veldt 65f2db860b Fix Hue docstring (#62684) 2021-12-23 17:22:36 +01:00
Marcel van der Veldt 8217b3981f Never use availability workaround for certified Hue devices (#62676) 2021-12-23 17:22:33 +01:00
Marcel van der Veldt 3076ead727 Fix Hue button events (#62669) 2021-12-23 17:22:30 +01:00
Marcel van der Veldt 094623e528 Adjust Hue retry logic to changes in the aiohue library (#62665) 2021-12-23 17:22:27 +01:00
Marcel van der Veldt cf528c5234 Bump aiohue to 3.0.10 (#62664) 2021-12-23 17:22:23 +01:00
Marcel van der Veldt 35cc2bf803 Bump aiohue to 3.0.9 (#62658) 2021-12-23 17:22:20 +01:00
Paulus Schoutsen 71bf4ad134 Bump aiohue to 3.0.8 (#62651) 2021-12-23 17:22:17 +01:00
Christian Manivong 7523044d65 Round Hue transition to steps of 100ms (#62619)
* Adding round() to transition before firing turn_on, turn_off #62608
2021-12-23 17:22:13 +01:00
Simone Chemelli d1110102c7 Fix missing exception handling from upstream lib in Fritz (#62617)
* Fix missing exception handling from upstream lib

* isort
2021-12-23 17:22:10 +01:00
J. Nick Koston 86c08d80c9 Bump flux_led to 0.27.13 to fix discovery of legacy devices (#62613)
- The 2013/2014 devices have yet another format for
  the version
2021-12-23 17:22:07 +01:00
Eugenio Panadero a66d63e439 Fix pvpc_hourly_pricing by changing data source and modernise integration (#62591) 2021-12-23 17:21:33 +01:00
Erik Montnemery 68f3f8db1e Improve google cast state reporting (#62587) 2021-12-23 17:16:46 +01:00
G Johansson 9d235618ff Fix timezone trafikverket_train (#62582)
* Bugfix trafikverket train

* Change from pytz to hass function

* Fix datetime in extra attributes

* Fix time timezone

* Reset changes extra attributes
2021-12-23 17:16:42 +01:00
Simone Chemelli d10c5f459f Fix missing object assignment for Fritz (#62575) 2021-12-23 17:16:38 +01:00
J. Nick Koston 9b3d44c255 Bump flux_led to 0.27.12 to fix legacy cct controllers (#62573) 2021-12-23 17:16:07 +01:00
Aaron Bach d10716ff55 Bump pytile to 2021.12.0 (#62559) 2021-12-23 17:14:25 +01:00
Marvin Wichmann 79dae30b19 Update xknx to version 0.18.15 (#62557) 2021-12-23 17:14:22 +01:00
jjlawren 2a515953ea Bump soco to 0.25.1 (#62523) 2021-12-23 17:14:18 +01:00
ShadowBr0ther b19dc8bc37 Fix repetier crash when printer is offline (#62490) 2021-12-23 17:13:51 +01:00
jjlawren 0b4bfcc941 Fix Sonos updating when entities are disabled (#62456)
Co-authored-by: J. Nick Koston <nick@koston.org>
2021-12-23 17:11:41 +01:00
Andre Richter 4f0c20cf33 Fix broken Vallox integration in 2021.12 (#62308) 2021-12-23 17:09:41 +01:00
schmyd 050bddb9fe Fix deconz light service parameter handling (#62128)
* Only check presence of values, not their content

* Add tests

* Revert "Only check presence of values, not their content"

This reverts commit 046f0ed5fd.

* Validate existence of keys, not their values

* Properly handle cases of missing keys
2021-12-23 17:09:38 +01:00
Angelo Gagliano ef9419f001 Require RPi.GPIO and bump adafruit-circuitpython-dht to 3.7.0 in dht (#61751) 2021-12-23 17:09:33 +01:00
Paulus Schoutsen 20a1bc710e Merge pull request #62366 from home-assistant/rc 2021-12-20 19:58:40 -08:00
Marcel van der Veldt 5e0ea9fd24 Change Hue availability blacklist logic a bit (#62446) 2021-12-20 16:09:32 -08:00
Marcel van der Veldt 1f0c13f259 bump aiohue to 3.0.7 (#62444) 2021-12-20 16:09:31 -08:00
rikroe a1fc223914 Bump bimmer_connected to 0.8.7 (#62435)
Co-authored-by: rikroe <rikroe@users.noreply.github.com>
2021-12-20 16:09:30 -08:00
Erik Montnemery 836d8a6fca Make it possible to turn on audio only google cast devices (#62420) 2021-12-20 11:18:28 -08:00
Franck Nijhof 520c3411dd Invalidate CI cache when bumping dependencies (#62394)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2021-12-20 10:36:39 -08:00
Franck Nijhof d90c107b1b Invalidate CI cache when bumping dependencies, part 2 (#62412) 2021-12-20 10:34:27 -08:00
Matthias Alphart 5a2bc8e493 Update xknx to 0.18.14 (#62411)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2021-12-20 10:26:27 -08:00
Erik Montnemery b8f8b30b9b Bump pychromecast to 10.2.2 (#62390) 2021-12-20 10:26:27 -08:00
Bram Kragten e80f4e03a4 Update frontend to 20211220.0 (#62389) 2021-12-20 10:26:26 -08:00
Eduard van Valkenburg 7ec369d8ef Bump brunt to 1.1.0 (#62386) 2021-12-20 10:25:50 -08:00
Erik Montnemery dcc08a0aac Don't use the homeassistant media app when casting media (#62385) 2021-12-20 10:24:40 -08:00
Paulus Schoutsen 4802e4e33f Bumped version to 2021.12.4 2021-12-19 22:32:50 -08:00
Paulus Schoutsen eb4b041d45 Bump voluptuous_serialize to 2.5.0 (#62363) 2021-12-19 22:32:37 -08:00
Eric Severance bfd8579566 Bump pywemo==0.7.0 (#62360) 2021-12-19 22:32:36 -08:00
Aaron Bach beb5a992e6 Ensure existing SimpliSafe websocket tasks are cancelled appropriately (#62347) 2021-12-19 22:32:36 -08:00
Maikel Punie 7c925778eb Fix velbus climate current temp (#62329) 2021-12-19 22:32:35 -08:00
Thijs Walcarius 7db161868e Fix missing brightness for Velbus entities (#62314)
* Fix #62169: missing brightness for Velbus-entities

* Use default implementation of supported_features

Co-authored-by: Thijs Walcarius <thijs.walcarius@ugent.be>
2021-12-19 22:32:34 -08:00
Paulus Schoutsen 5f2a2280c5 Bump ring to 0.7.2 (#62299) 2021-12-19 22:32:33 -08:00
Michael Chisholm b327628b6e Update async-upnp-client library to 0.23.1 (#62298) 2021-12-19 22:32:20 -08:00
Diego Elio Pettenò 311ebd4a96 Bump async-upnp-client to 0.23.0 (#62223) 2021-12-19 22:31:18 -08:00
J. Nick Koston dc4659b167 Bump flux_led to 0.27.8 to fix discovery of older devices (#62292) 2021-12-19 22:29:51 -08:00
J. Nick Koston c1d0fe9eae Fix Non-thread-safe operation in zwave node_added (#62287) 2021-12-19 22:29:50 -08:00
J. Nick Koston 3fde6bfd73 Fix Non-thread-safe operation in rflink binary_sensor (#62286) 2021-12-19 22:29:49 -08:00
Martin Hjelmare 4efa3b634e Fix fitbit no SSL URL handling (#62270) 2021-12-19 22:29:47 -08:00
Franck Nijhof cd65aaee60 Upgrade tailscale to 0.1.6 (#62267) 2021-12-19 22:29:47 -08:00
Simone Chemelli 2395c753fe Fix logging for Shelly climate platform (#62264)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2021-12-19 22:29:46 -08:00
starkillerOG bc2949ef31 bump pynetgear to 0.8.0 (#62261) 2021-12-19 22:29:45 -08:00
Aaron Bach f665c4e588 Fix bug in which SimpliSafe websocket won't reconnect on error (#62241) 2021-12-19 22:29:45 -08:00
Aaron Bach 57b7b28d60 Fix spurious RainMachine config entry reload (#62215) 2021-12-19 22:29:44 -08:00
Gage Benne 9a0f42f9a7 Bump pydexcom to 0.2.2 (#62207) 2021-12-19 22:29:43 -08:00
Aidan Timson 895dcaf690 Force Lyric token refresh on first authentication failure (#62100) 2021-12-19 22:29:43 -08:00
J. Nick Koston 4e96ff78b5 Avoid setting nexia humidity to the same value since it causes the api to fail (#61843) 2021-12-19 22:29:42 -08:00
Matthias Alphart a87ed13a04 Silently retry Fronius inverter endpoint 2 times (#61826) 2021-12-19 22:29:42 -08:00
jkuettner 735deff45e Fix "vevent" KeyError in caldav component (#61718) 2021-12-19 22:29:41 -08:00
Hans Oischinger 22867acaf8 Add vicare strings (#61593)
* Add vicare strings

* Remove duplicates

* Remove duplicates from english translation

* Add missing strings
2021-12-19 22:29:40 -08:00
RDFurman c507c72350 Honeywell unique id fix (#59393)
* Move error logging and remove reload

* Change device assignment and improve logging

* Use dictionary for devices

* Check if new device exists in API response

* Add test and make loop better

* Make test assert on error in log
2021-12-19 22:29:40 -08:00
Franck Nijhof 7fc36c4fe0 Merge pull request #62182 from home-assistant/rc 2021-12-17 13:43:37 +01:00
Franck Nijhof 5196a770cc Bumped version to 2021.12.3 2021-12-17 11:43:38 +01:00
Erik Montnemery 54d7380f4d Fix threading error in zha (#62170) 2021-12-17 11:43:04 +01:00
Erik Montnemery c445e93d45 Fix threading error in scripts with repeat or choose actions (#62168) 2021-12-17 11:43:00 +01:00
Marcel van der Veldt 614529d7c3 Add guard in call to activate_scene in Hue (#62177) 2021-12-17 11:41:04 +01:00
Allen Porter 9361c9ef60 Bump google-nest-sdm to 0.4.9 (#62160) 2021-12-17 11:41:00 +01:00
J. Nick Koston 19a0644b50 Fix Non-thread-safe operation in logbook (#62148) 2021-12-17 11:40:57 +01:00
J. Nick Koston 82173f477c Fix Non-thread-safe operation in homekit light events (#62147) 2021-12-17 11:40:54 +01:00
Simone Chemelli b4af32624d Improve availability for Shelly Valve (#62129) 2021-12-17 11:40:50 +01:00
Maximilian 78f40bd4bf Add missing timezone information (#62106) 2021-12-17 11:40:47 +01:00
Erik Montnemery d92ad76ed9 Fix none-check in template light (#62089) 2021-12-17 11:40:44 +01:00
J. Nick Koston e44d50e1b1 Bump flux_led to 0.26.15 (#62017) 2021-12-17 11:40:40 +01:00
Eduard van Valkenburg 95c0eeecfb Brunt dependency bump to 1.0.2 (#62014) 2021-12-17 11:40:37 +01:00
Marcel van der Veldt ec263840ba Bump aiohue to 3.0.6 (#61974) 2021-12-17 11:40:34 +01:00
Marvin Wichmann 8047134c88 Fix notify platform setup for KNX (#61842)
* Fix notify platform setup for KNX

* Apply review suggestions

* Store hass config in DATA_HASS_CONFIG

* Readd guard clause
2021-12-17 11:40:31 +01:00
epenet cb89688873 Fix OwnetError preventing onewire initialisation (#61696)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2021-12-17 11:40:27 +01:00
Simone Chemelli c73319e162 Add restore logic to Shelly climate platform (#61632)
* Add restore logic to Shelly climate platform

* Handle missing channel on restore
2021-12-17 11:39:47 +01:00
Ian 499cc2e51d Nextbus upcoming sort as integer (#61416) 2021-12-17 11:33:21 +01:00
sindudas 5a03fffc20 Update ebusdpy version (#59899) 2021-12-17 11:33:17 +01:00
Franck Nijhof 6d8d472f0f Merge pull request #61902 from home-assistant/rc 2021-12-15 17:02:35 +01:00
Franck Nijhof ac2897fc67 Bumped version to 2021.12.2 2021-12-15 16:04:48 +01:00
Bram Kragten e7e20533bd Update frontend to 20211215.0 (#61877) 2021-12-15 16:03:37 +01:00
Marcel van der Veldt 2772bae2e1 Bump aiohue to 3.0.5 (#61875) 2021-12-15 16:03:34 +01:00
Allen Porter 86622794e0 Bump google-nest-sdm to 0.4.8 (#61851) 2021-12-15 16:03:30 +01:00
Michael Davie 686f6768fc Fix broken Environment Canada (#61848) 2021-12-15 16:03:27 +01:00
Marvin Wichmann f271fea07c Allow setting local_ip for knx routing connections (#61836) 2021-12-15 16:03:24 +01:00
Aaron Bach 77b1df5902 Ensure SimpliSafe websocket reconnects upon new token (#61835) 2021-12-15 16:03:20 +01:00
Teemu R 1faa111222 Bump python-miio to 0.5.9.2 (#61831) 2021-12-15 16:03:17 +01:00
Daniel Hjelseth Høyer b513301363 Tibber, update library, fixes #61525 (#61813) 2021-12-15 16:03:14 +01:00
Erik Montnemery 32bdcdd663 Bump pychromecast to 10.2.1 (#61811) 2021-12-15 16:03:11 +01:00
Erik Montnemery 40f76d4ed9 Don't override pychromecast MediaController's APP ID (#61796) 2021-12-15 16:03:07 +01:00
MattWestb 34568aad89 Fix ZHA unoccupied setpoints. (#61791)
ATTR_UNOCCP_HEAT_SETPT and ATTR_UNOCCP_COOL_SETPT is mixed up. 
Fixing so heating is heating and cooling is colling.
2021-12-15 16:03:04 +01:00
Eduard van Valkenburg ffe84e8ece Bump brunt package to 1.0.1 (#61784) 2021-12-15 16:03:01 +01:00
Franck Nijhof 8cbd89282b Upgrade tailscale to 0.1.5 (#61744) 2021-12-15 16:02:58 +01:00
Marcel van der Veldt 1467668c94 Blacklist availability check for a light at startup in Hue integration (#61737) 2021-12-15 16:02:55 +01:00
Marcel van der Veldt bbef38964d Fix Flash effect for Hue lights (#61733) 2021-12-15 16:02:52 +01:00
Marcel van der Veldt 03b88af032 Fix turn_off with transition for grouped Hue lights (#61728)
* fix turn_off with transition for grouped hue lights

* add test
2021-12-15 16:02:49 +01:00
Marcel van der Veldt 0626bc8b4f Add check for incompatible device trigger in Hue integration (#61726) 2021-12-15 16:02:46 +01:00
Vilppu Vuorinen 37ecbc53a7 Update pymelcloud to 2.5.6 (#61717) 2021-12-15 16:02:43 +01:00
Paulus Schoutsen 52c96654a4 Bump aiohue to 3.0.4 (#61709) 2021-12-15 16:02:39 +01:00
Joakim Sørensen 791c2f4b8a Add additional-tag to machine builds (#61693) 2021-12-15 16:02:36 +01:00
Austin Mroczek ed041d5b7c Bump total_connect_client to 2021.12 (#61634) 2021-12-15 16:02:33 +01:00
Allen Porter 1833ab96dc Suppress errors for legacy nest api when using media source (#61629) 2021-12-15 16:02:29 +01:00
majuss ff2e2656b3 Upgrade lupupy to 0.0.24 (#61598) 2021-12-15 16:02:26 +01:00
bsmappee 599c20c76e Bump pysmappee to 0.2.29 (#61160) 2021-12-15 16:02:19 +01:00
Paulus Schoutsen 5df747276f Merge pull request #61625 from home-assistant/rc 2021-12-12 15:39:42 -08:00
Paulus Schoutsen 77b06bc158 Bump aiohue to 3.0.3 (#61627) 2021-12-12 14:29:37 -08:00
J. Nick Koston db6d176658 Bump aiopvapi to 1.6.19 to fix async_timeout passing loop (#61618) 2021-12-12 14:29:36 -08:00
Paulus Schoutsen 973eb4f6d4 Bumped version to 2021.12.1 2021-12-12 14:15:15 -08:00
Marcel van der Veldt 14401aa840 Fix availability for 3th party Hue lights (#61603) 2021-12-12 14:14:13 -08:00
Ernst Klamer b82ddb77bc Fix for failing Solarlog integration in HA 2021.12 (#61602) 2021-12-12 14:14:13 -08:00
Allen Porter 22530f72f3 Only publish nest camera event messages once per thread and bump nest version (#61587) 2021-12-12 14:14:12 -08:00
Marcel van der Veldt a16bf358aa enable grouped light if enabled in previous integration (#61582) 2021-12-12 14:14:12 -08:00
Marcel van der Veldt 0924874d4b Fix Hue transition calculation (#61581) 2021-12-12 14:14:11 -08:00
Bram Kragten a3ff783bc1 Update frontend to 20211212.0 (#61577) 2021-12-12 14:14:10 -08:00
J. Nick Koston e7d06e3f6a Fix HomeKit covers with device class window and no tilt (#61566) 2021-12-12 14:14:10 -08:00
Allen Porter a2fc870266 Update logic for nest media source can_play for events (#61537) 2021-12-12 14:14:09 -08:00
jjlawren ffcb107716 Fix Sonos sub & surround switch state reporting (#61531)
* Fix sub/surround states, refactor volume param handling

* Lint
2021-12-12 14:14:08 -08:00
135 changed files with 1976 additions and 3833 deletions
+13 -2
View File
@@ -131,7 +131,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2021.11.4
uses: home-assistant/builder@2021.12.0
with:
args: |
$BUILD_ARGS \
@@ -170,6 +170,17 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v2.4.0
- name: Set build additional args
run: |
# Create general tags
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
else
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
fi
- name: Login to DockerHub
uses: docker/login-action@v1.10.0
with:
@@ -184,7 +195,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2021.11.4
uses: home-assistant/builder@2021.12.0
with:
args: |
$BUILD_ARGS \
+18 -8
View File
@@ -155,10 +155,15 @@ jobs:
key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-python-key.outputs.key }}
restore-keys: |
${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-
# Temporary disabling the restore of environments when bumping
# a dependency. It seems that we are experiencing issues with
# restoring environments in GitHub Actions, although unclear why.
# First attempt: https://github.com/home-assistant/core/pull/62383
#
# restore-keys: |
# ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-
# ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-
# ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -517,10 +522,15 @@ jobs:
key: >-
${{ runner.os }}-${{ matrix.python-version }}-${{
steps.generate-python-key.outputs.key }}
restore-keys: |
${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}-
${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-
${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-
# Temporary disabling the restore of environments when bumping
# a dependency. It seems that we are experiencing issues with
# restoring environments in GitHub Actions, although unclear why.
# First attempt: https://github.com/home-assistant/core/pull/62383
#
# restore-keys: |
# ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}-
# ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-
# ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-
- name: Create full Python ${{ matrix.python-version }} virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
+1 -1
View File
@@ -432,7 +432,7 @@ homeassistant/components/recollect_waste/* @bachya
homeassistant/components/recorder/* @home-assistant/core
homeassistant/components/rejseplanen/* @DarkFox
homeassistant/components/renault/* @epenet
homeassistant/components/repetier/* @MTrab
homeassistant/components/repetier/* @MTrab @ShadowBr0ther
homeassistant/components/rflink/* @javicalle
homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221
homeassistant/components/ridwell/* @bachya
@@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": ["bimmer_connected==0.8.5"],
"requirements": ["bimmer_connected==0.8.7"],
"codeowners": ["@gerard33", "@rikroe"],
"config_flow": true,
"iot_class": "cloud_polling"
+1 -1
View File
@@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
async with async_timeout.timeout(10):
things = await bapi.async_get_things(force=True)
return {thing.SERIAL: thing for thing in things}
return {thing.serial: thing for thing in things}
except ServerDisconnectedError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
except ClientResponseError as err:
+9 -12
View File
@@ -100,7 +100,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
self._remove_update_listener = None
self._attr_name = self._thing.NAME
self._attr_name = self._thing.name
self._attr_device_class = DEVICE_CLASS_SHADE
self._attr_supported_features = COVER_FEATURES
self._attr_attribution = ATTRIBUTION
@@ -109,8 +109,8 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
name=self._attr_name,
via_device=(DOMAIN, self._entry_id),
manufacturer="Brunt",
sw_version=self._thing.FW_VERSION,
model=self._thing.MODEL,
sw_version=self._thing.fw_version,
model=self._thing.model,
)
async def async_added_to_hass(self) -> None:
@@ -127,8 +127,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
None is unknown, 0 is closed, 100 is fully open.
"""
pos = self.coordinator.data[self.unique_id].currentPosition
return int(pos) if pos is not None else None
return self.coordinator.data[self.unique_id].current_position
@property
def request_cover_position(self) -> int | None:
@@ -139,8 +138,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
to Brunt, at times there is a diff of 1 to current
None is unknown, 0 is closed, 100 is fully open.
"""
pos = self.coordinator.data[self.unique_id].requestPosition
return int(pos) if pos is not None else None
return self.coordinator.data[self.unique_id].request_position
@property
def move_state(self) -> int | None:
@@ -149,8 +147,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
None is unknown, 0 when stopped, 1 when opening, 2 when closing
"""
mov = self.coordinator.data[self.unique_id].moveState
return int(mov) if mov is not None else None
return self.coordinator.data[self.unique_id].move_state
@property
def is_opening(self) -> bool:
@@ -190,11 +187,11 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
"""Set the cover to the new position and wait for the update to be reflected."""
try:
await self._bapi.async_change_request_position(
position, thingUri=self._thing.thingUri
position, thing_uri=self._thing.thing_uri
)
except ClientResponseError as exc:
raise HomeAssistantError(
f"Unable to reposition {self._thing.NAME}"
f"Unable to reposition {self._thing.name}"
) from exc
self.coordinator.update_interval = FAST_INTERVAL
await self.coordinator.async_request_refresh()
@@ -204,7 +201,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
"""Update the update interval after each refresh."""
if (
self.request_cover_position
== self._bapi.last_requested_positions[self._thing.thingUri]
== self._bapi.last_requested_positions[self._thing.thing_uri]
and self.move_state == 0
):
self.coordinator.update_interval = REGULAR_INTERVAL
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Brunt Blind Engine",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/brunt",
"requirements": ["brunt==1.0.0"],
"requirements": ["brunt==1.1.0"],
"codeowners": ["@eavanvalkenburg"],
"iot_class": "cloud_polling"
}
@@ -161,6 +161,9 @@ class WebDavCalendarData:
)
event_list = []
for event in vevent_list:
if not hasattr(event.instance, "vevent"):
_LOGGER.warning("Skipped event with missing 'vevent' property")
continue
vevent = event.instance.vevent
if not self.is_matching(vevent, self.search):
continue
@@ -198,6 +201,9 @@ class WebDavCalendarData:
# and they would not be properly parsed using their original start/end dates.
new_events = []
for event in results:
if not hasattr(event.instance, "vevent"):
_LOGGER.warning("Skipped event with missing 'vevent' property")
continue
vevent = event.instance.vevent
for start_dt in vevent.getrruleset() or []:
_start_of_today = start_of_today
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==10.1.1"],
"requirements": ["pychromecast==10.2.2"],
"after_dependencies": [
"cloud",
"http",
+16 -12
View File
@@ -47,7 +47,6 @@ from homeassistant.components.plex.const import PLEX_URI_SCHEME
from homeassistant.components.plex.services import lookup_plex_media
from homeassistant.const import (
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
CAST_APP_ID_HOMEASSISTANT_MEDIA,
EVENT_HOMEASSISTANT_STOP,
STATE_IDLE,
STATE_OFF,
@@ -77,6 +76,8 @@ from .helpers import CastStatusListener, ChromecastInfo, ChromeCastZeroconf
_LOGGER = logging.getLogger(__name__)
APP_IDS_UNRELIABLE_MEDIA_INFO = ("Netflix",)
CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png"
SUPPORT_CAST = SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF
@@ -230,7 +231,6 @@ class CastDevice(MediaPlayerEntity):
self._cast_info.cast_info,
ChromeCastZeroconf.get_zeroconf(),
)
chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA
self._chromecast = chromecast
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
@@ -397,11 +397,14 @@ class CastDevice(MediaPlayerEntity):
return
if self._chromecast.app_id is not None:
# Quit the previous app before starting splash screen
# Quit the previous app before starting splash screen or media player
self._chromecast.quit_app()
# The only way we can turn the Chromecast is on is by launching an app
self._chromecast.play_media(CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED)
if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST:
self._chromecast.play_media(CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED)
else:
self._chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER)
def turn_off(self):
"""Turn off the cast device."""
@@ -527,9 +530,8 @@ class CastDevice(MediaPlayerEntity):
self._chromecast.register_handler(controller)
controller.play_media(media)
else:
self._chromecast.media_controller.play_media(
media_id, media_type, **kwargs.get(ATTR_MEDIA_EXTRA, {})
)
app_data = {"media_id": media_id, "media_type": media_type, **extra}
quick_play(self._chromecast, "default_media_receiver", app_data)
def _media_status(self):
"""
@@ -564,7 +566,10 @@ class CastDevice(MediaPlayerEntity):
if media_status.player_is_idle:
return STATE_IDLE
if self.app_id is not None and self.app_id != pychromecast.IDLE_APP_ID:
return STATE_PLAYING
if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO:
# Some apps don't report media status, show the player as playing
return STATE_PLAYING
return STATE_IDLE
if self._chromecast is not None and self._chromecast.is_idle:
return STATE_OFF
return None
@@ -677,9 +682,9 @@ class CastDevice(MediaPlayerEntity):
support = SUPPORT_CAST
media_status = self._media_status()[0]
if (
self._chromecast
and self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST
if self._chromecast and self._chromecast.cast_type in (
pychromecast.const.CAST_TYPE_CHROMECAST,
pychromecast.const.CAST_TYPE_AUDIO,
):
support |= SUPPORT_TURN_ON
@@ -820,7 +825,6 @@ class DynamicCastGroup:
self._cast_info.cast_info,
ChromeCastZeroconf.get_zeroconf(),
)
chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA
self._chromecast = chromecast
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
+7 -7
View File
@@ -199,7 +199,7 @@ class DeconzBaseLight(DeconzDevice, LightEntity):
"""Turn on light."""
data: dict[str, bool | float | int | str | tuple[float, float]] = {"on": True}
if attr_brightness := kwargs.get(ATTR_BRIGHTNESS):
if (attr_brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None:
data["brightness"] = attr_brightness
if attr_color_temp := kwargs.get(ATTR_COLOR_TEMP):
@@ -215,16 +215,16 @@ class DeconzBaseLight(DeconzDevice, LightEntity):
if ATTR_XY_COLOR in kwargs:
data["xy"] = kwargs[ATTR_XY_COLOR]
if attr_transition := kwargs.get(ATTR_TRANSITION):
if (attr_transition := kwargs.get(ATTR_TRANSITION)) is not None:
data["transition_time"] = int(attr_transition * 10)
elif "IKEA" in self._device.manufacturer:
data["transition_time"] = 0
if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH, ""))) is not None:
if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None:
data["alert"] = alert
del data["on"]
if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT, ""))) is not None:
if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT))) is not None:
data["effect"] = effect
await self._device.set_state(**data)
@@ -236,11 +236,11 @@ class DeconzBaseLight(DeconzDevice, LightEntity):
data: dict[str, bool | int | str] = {"on": False}
if ATTR_TRANSITION in kwargs:
if (attr_transition := kwargs.get(ATTR_TRANSITION)) is not None:
data["brightness"] = 0
data["transition_time"] = int(kwargs[ATTR_TRANSITION] * 10)
data["transition_time"] = int(attr_transition * 10)
if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH, ""))) is not None:
if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None:
data["alert"] = alert
del data["on"]
@@ -3,7 +3,7 @@
"name": "Dexcom",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dexcom",
"requirements": ["pydexcom==0.2.1"],
"requirements": ["pydexcom==0.2.2"],
"codeowners": ["@gagebenne"],
"iot_class": "cloud_polling"
}
+8 -3
View File
@@ -2,7 +2,12 @@
"domain": "dht",
"name": "DHT Sensor",
"documentation": "https://www.home-assistant.io/integrations/dht",
"requirements": ["adafruit-circuitpython-dht==3.6.0"],
"codeowners": ["@thegardenmonkey"],
"requirements": [
"adafruit-circuitpython-dht==3.7.0",
"RPi.GPIO==0.7.1a4"
],
"codeowners": [
"@thegardenmonkey"
],
"iot_class": "local_polling"
}
}
@@ -3,7 +3,7 @@
"name": "DLNA Digital Media Renderer",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"requirements": ["async-upnp-client==0.22.12"],
"requirements": ["async-upnp-client==0.23.1"],
"dependencies": ["ssdp"],
"ssdp": [
{
+1 -1
View File
@@ -2,7 +2,7 @@
"domain": "ebusd",
"name": "ebusd",
"documentation": "https://www.home-assistant.io/integrations/ebusd",
"requirements": ["ebusdpy==0.0.16"],
"requirements": ["ebusdpy==0.0.17"],
"codeowners": [],
"iot_class": "local_polling"
}
@@ -2,7 +2,7 @@
"domain": "environment_canada",
"name": "Environment Canada",
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"requirements": ["env_canada==0.5.18"],
"requirements": ["env_canada==0.5.20"],
"codeowners": ["@gwww", "@michaeldavie"],
"config_flow": true,
"iot_class": "cloud_polling"
+5 -4
View File
@@ -112,10 +112,11 @@ def request_app_setup(
Then come back here and hit the below button.
"""
except NoURLAvailableError:
error_msg = """Could not find a SSL enabled URL for your Home Assistant instance.
Fitbit requires that your Home Assistant instance is accessible via HTTPS.
"""
configurator.notify_errors(_CONFIGURING["fitbit"], error_msg)
_LOGGER.error(
"Could not find an SSL enabled URL for your Home Assistant instance. "
"Fitbit requires that your Home Assistant instance is accessible via HTTPS"
)
return
submit = "I have saved my Client ID and Client Secret into fitbit.conf."
@@ -94,6 +94,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
firmware_date=None,
model_info=None,
model_description=None,
remote_access_enabled=None,
remote_access_host=None,
remote_access_port=None,
)
return await self._async_handle_discovery()
@@ -261,6 +264,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
firmware_date=None,
model_info=None,
model_description=bulb.model_data.description,
remote_access_enabled=None,
remote_access_host=None,
remote_access_port=None,
)
@@ -3,7 +3,7 @@
"name": "Flux LED/MagicHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.26.7"],
"requirements": ["flux_led==0.27.13"],
"quality_scale": "platinum",
"codeowners": ["@icemanch"],
"iot_class": "local_push",
+2 -1
View File
@@ -3,6 +3,7 @@ import logging
from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError
from fritzconnection.core.logger import fritzlogger
from requests import exceptions
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -45,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await fritz_tools.async_start(entry.options)
except FritzSecurityError as ex:
raise ConfigEntryAuthFailed from ex
except FritzConnectionException as ex:
except (FritzConnectionException, exceptions.ConnectionError) as ex:
raise ConfigEntryNotReady from ex
hass.data.setdefault(DOMAIN, {})
@@ -117,7 +117,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
self._port = ssdp_location.port
self._name = (
discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)
or self.fritz_tools.model
or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME]
)
self.context[CONF_HOST] = self._host
@@ -5,7 +5,7 @@ from abc import ABC, abstractmethod
from datetime import timedelta
from typing import TYPE_CHECKING, Any, Dict, TypeVar
from pyfronius import FroniusError
from pyfronius import BadStatusError, FroniusError
from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.core import callback
@@ -43,6 +43,8 @@ class FroniusCoordinatorBase(
error_interval: timedelta
valid_descriptions: list[SensorEntityDescription]
MAX_FAILED_UPDATES = 3
def __init__(self, *args: Any, solar_net: FroniusSolarNet, **kwargs: Any) -> None:
"""Set up the FroniusCoordinatorBase class."""
self._failed_update_count = 0
@@ -62,7 +64,7 @@ class FroniusCoordinatorBase(
data = await self._update_method()
except FroniusError as err:
self._failed_update_count += 1
if self._failed_update_count == 3:
if self._failed_update_count == self.MAX_FAILED_UPDATES:
self.update_interval = self.error_interval
raise UpdateFailed(err) from err
@@ -116,6 +118,8 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase):
error_interval = timedelta(minutes=10)
valid_descriptions = INVERTER_ENTITY_DESCRIPTIONS
SILENT_RETRIES = 3
def __init__(
self, *args: Any, inverter_info: FroniusDeviceInfo, **kwargs: Any
) -> None:
@@ -125,9 +129,19 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase):
async def _update_method(self) -> dict[SolarNetId, Any]:
"""Return data per solar net id from pyfronius."""
data = await self.solar_net.fronius.current_inverter_data(
self.inverter_info.solar_net_id
)
# almost 1% of `current_inverter_data` requests on Symo devices result in
# `BadStatusError Code: 8 - LNRequestTimeout` due to flaky internal
# communication between the logger and the inverter.
for silent_retry in range(self.SILENT_RETRIES):
try:
data = await self.solar_net.fronius.current_inverter_data(
self.inverter_info.solar_net_id
)
except BadStatusError as err:
if silent_retry == (self.SILENT_RETRIES - 1):
raise err
continue
break
# wrap a single devices data in a dict with solar_net_id key for
# FroniusCoordinatorBase _async_update_data and add_entities_for_seen_keys
return {self.inverter_info.solar_net_id: data}
@@ -2,7 +2,9 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20211211.0"],
"requirements": [
"home-assistant-frontend==20211220.0"
],
"dependencies": [
"api",
"auth",
@@ -15,6 +17,8 @@
"system_log",
"websocket_api"
],
"codeowners": ["@home-assistant/frontend"],
"codeowners": [
"@home-assistant/frontend"
],
"quality_scale": "internal"
}
}
@@ -249,14 +249,17 @@ class OpeningDeviceBase(HomeAccessory):
def async_update_state(self, new_state):
"""Update cover position and tilt after state changed."""
# update tilt
if not self._supports_tilt:
return
current_tilt = new_state.attributes.get(ATTR_CURRENT_TILT_POSITION)
if isinstance(current_tilt, (float, int)):
# HomeKit sends values between -90 and 90.
# We'll have to normalize to [0,100]
current_tilt = (current_tilt / 100.0 * 180.0) - 90.0
current_tilt = int(current_tilt)
self.char_current_tilt.set_value(current_tilt)
self.char_target_tilt.set_value(current_tilt)
if not isinstance(current_tilt, (float, int)):
return
# HomeKit sends values between -90 and 90.
# We'll have to normalize to [0,100]
current_tilt = (current_tilt / 100.0 * 180.0) - 90.0
current_tilt = int(current_tilt)
self.char_current_tilt.set_value(current_tilt)
self.char_target_tilt.set_value(current_tilt)
class OpeningDevice(OpeningDeviceBase, HomeAccessory):
@@ -120,10 +120,11 @@ class Light(HomeAccessory):
if self._event_timer:
self._event_timer()
self._event_timer = async_call_later(
self.hass, CHANGE_COALESCE_TIME_WINDOW, self._send_events
self.hass, CHANGE_COALESCE_TIME_WINDOW, self._async_send_events
)
def _send_events(self, *_):
@callback
def _async_send_events(self, *_):
"""Process all changes at once."""
_LOGGER.debug("Coalesced _set_chars: %s", self._pending_events)
char_values = self._pending_events
+23 -12
View File
@@ -30,14 +30,13 @@ async def async_setup_entry(hass, config):
loc_id = config.data.get(CONF_LOC_ID)
dev_id = config.data.get(CONF_DEV_ID)
devices = []
devices = {}
for location in client.locations_by_id.values():
for device in location.devices_by_id.values():
if (not loc_id or location.locationid == loc_id) and (
not dev_id or device.deviceid == dev_id
):
devices.append(device)
if not loc_id or location.locationid == loc_id:
for device in location.devices_by_id.values():
if not dev_id or device.deviceid == dev_id:
devices[device.deviceid] = device
if len(devices) == 0:
_LOGGER.debug("No devices found")
@@ -107,23 +106,30 @@ class HoneywellData:
if self._client is None:
return False
devices = [
refreshed_devices = [
device
for location in self._client.locations_by_id.values()
for device in location.devices_by_id.values()
]
if len(devices) == 0:
_LOGGER.error("Failed to find any devices")
if len(refreshed_devices) == 0:
_LOGGER.error("Failed to find any devices after retry")
return False
self.devices = devices
for updated_device in refreshed_devices:
if updated_device.deviceid in self.devices:
self.devices[updated_device.deviceid] = updated_device
else:
_LOGGER.info(
"New device with ID %s detected, reload the honeywell integration if you want to access it in Home Assistant"
)
await self._hass.config_entries.async_reload(self._config.entry_id)
return True
async def _refresh_devices(self):
"""Refresh each enabled device."""
for device in self.devices:
for device in self.devices.values():
await self._hass.async_add_executor_job(device.refresh)
await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME)
@@ -143,11 +149,16 @@ class HoneywellData:
) as exp:
retries -= 1
if retries == 0:
_LOGGER.error(
"Ran out of retry attempts (3 attempts allocated). Error: %s",
exp,
)
raise exp
result = await self._retry()
if not result:
_LOGGER.error("Retry result was empty. Error: %s", exp)
raise exp
_LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp)
_LOGGER.info("SomeComfort update failed, retrying. Error: %s", exp)
@@ -122,7 +122,7 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=Non
async_add_entities(
[
HoneywellUSThermostat(data, device, cool_away_temp, heat_away_temp)
for device in data.devices
for device in data.devices.values()
]
)
+15 -49
View File
@@ -3,13 +3,12 @@ from __future__ import annotations
import asyncio
from collections.abc import Callable
from http import HTTPStatus
import logging
from typing import Any
from aiohttp import client_exceptions
from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized
from aiohue.errors import AiohueException
from aiohue.errors import AiohueException, BridgeBusy
import async_timeout
from homeassistant import core
@@ -38,9 +37,6 @@ class HueBridge:
self.config_entry = config_entry
self.hass = hass
self.authorized = False
self.parallel_updates_semaphore = asyncio.Semaphore(
3 if self.api_version == 1 else 10
)
# Jobs to be executed when API is reset.
self.reset_jobs: list[core.CALLBACK_TYPE] = []
self.sensor_manager: SensorManager | None = None
@@ -83,6 +79,7 @@ class HueBridge:
client_exceptions.ClientOSError,
client_exceptions.ServerDisconnectedError,
client_exceptions.ContentTypeError,
BridgeBusy,
) as err:
raise ConfigEntryNotReady(
f"Error connecting to the Hue bridge at {self.host}"
@@ -115,50 +112,19 @@ class HueBridge:
async def async_request_call(
self, task: Callable, *args, allowed_errors: list[str] | None = None, **kwargs
) -> Any:
"""Limit parallel requests to Hue hub.
The Hue hub can only handle a certain amount of parallel requests, total.
Although we limit our parallel requests, we still will run into issues because
other products are hitting up Hue.
ClientOSError means hub closed the socket on us.
ContentResponseError means hub raised an error.
Since we don't make bad requests, this is on them.
"""
max_tries = 5
async with self.parallel_updates_semaphore:
for tries in range(max_tries):
try:
return await task(*args, **kwargs)
except AiohueException as err:
# The new V2 api is a bit more fanatic with throwing errors
# some of which we accept in certain conditions
# handle that here. Note that these errors are strings and do not have
# an identifier or something.
if allowed_errors is not None and str(err) in allowed_errors:
# log only
self.logger.debug(
"Ignored error/warning from Hue API: %s", str(err)
)
return None
raise err
except (
client_exceptions.ClientOSError,
client_exceptions.ClientResponseError,
client_exceptions.ServerDisconnectedError,
) as err:
if tries == max_tries:
self.logger.error("Request failed %s times, giving up", tries)
raise
# We only retry if it's a server error. So raise on all 4XX errors.
if (
isinstance(err, client_exceptions.ClientResponseError)
and err.status < HTTPStatus.INTERNAL_SERVER_ERROR
):
raise
await asyncio.sleep(HUB_BUSY_SLEEP * tries)
"""Send request to the Hue bridge, optionally omitting error(s)."""
try:
return await task(*args, **kwargs)
except AiohueException as err:
# The (new) Hue api can be a bit fanatic with throwing errors
# some of which we accept in certain conditions
# handle that here. Note that these errors are strings and do not have
# an identifier or something.
if allowed_errors is not None and str(err) in allowed_errors:
# log only
self.logger.debug("Ignored error/warning from Hue API: %s", str(err))
return None
raise err
async def async_reset(self) -> bool:
"""Reset this bridge to default state.
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": ["aiohue==3.0.2"],
"requirements": ["aiohue==3.0.10"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",
+4 -4
View File
@@ -8,6 +8,7 @@ from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.scenes import ScenesController
from aiohue.v2.models.scene import Scene as HueScene
from homeassistant.components.light import ATTR_TRANSITION
from homeassistant.components.scene import Scene as SceneEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
@@ -16,6 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .bridge import HueBridge
from .const import DOMAIN
from .v2.entity import HueBaseEntity
from .v2.helpers import normalize_hue_transition
async def async_setup_entry(
@@ -94,11 +96,9 @@ class HueSceneEntity(HueBaseEntity, SceneEntity):
async def async_activate(self, **kwargs: Any) -> None:
"""Activate Hue scene."""
transition = kwargs.get("transition")
if transition is not None:
# hue transition duration is in steps of 100 ms
transition = int(transition * 100)
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
dynamic = kwargs.get("dynamic", self.is_dynamic)
await self.bridge.async_request_call(
self.controller.recall,
self.resource.id,
+4 -2
View File
@@ -146,8 +146,10 @@ async def hue_activate_scene_v2(
continue
# found match!
if transition:
transition = transition * 100 # in steps of 100ms
await api.scenes.recall(scene.id, dynamic=dynamic, duration=transition)
transition = transition * 1000 # transition is in ms
await bridge.async_request_call(
api.scenes.recall, scene.id, dynamic=dynamic, duration=transition
)
return True
LOGGER.debug(
"Unable to find scene %s for group %s on bridge %s",
@@ -7,6 +7,7 @@ from aiohue.v2.models.button import ButtonEvent
from aiohue.v2.models.resource import ResourceTypes
import voluptuous as vol
from homeassistant.components import persistent_notification
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import event as event_trigger
from homeassistant.const import (
@@ -35,7 +36,7 @@ if TYPE_CHECKING:
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): str,
vol.Required(CONF_SUBTYPE): int,
vol.Required(CONF_SUBTYPE): vol.Union(int, str),
vol.Optional(CONF_UNIQUE_ID): str,
}
)
@@ -54,6 +55,33 @@ DEVICE_SPECIFIC_EVENT_TYPES = {
}
def check_invalid_device_trigger(
bridge: HueBridge,
config: ConfigType,
device_entry: DeviceEntry,
automation_info: AutomationTriggerInfo | None = None,
):
"""Check automation config for deprecated format."""
# NOTE: Remove this check after 2022.6
if isinstance(config["subtype"], int):
return
# found deprecated V1 style trigger, notify the user that it should be adjusted
msg = (
f"Incompatible device trigger detected for "
f"[{device_entry.name}](/config/devices/device/{device_entry.id}) "
"Please manually fix the outdated automation(s) once to fix this issue."
)
if automation_info:
automation_id = automation_info["variables"]["this"]["attributes"]["id"] # type: ignore
msg += f"\n\n[Check it out](/config/automation/edit/{automation_id})."
persistent_notification.async_create(
bridge.hass,
msg,
title="Outdated device trigger found",
notification_id=f"hue_trigger_{device_entry.id}",
)
async def async_validate_trigger_config(
bridge: "HueBridge",
device_entry: DeviceEntry,
@@ -61,6 +89,7 @@ async def async_validate_trigger_config(
):
"""Validate config."""
config = TRIGGER_SCHEMA(config)
check_invalid_device_trigger(bridge, config, device_entry)
return config
@@ -84,6 +113,7 @@ async def async_attach_trigger(
},
}
)
check_invalid_device_trigger(bridge, config, device_entry, automation_info)
return await event_trigger.async_attach_trigger(
hass, event_config, action, automation_info, platform_type="device"
)
+55 -1
View File
@@ -47,6 +47,9 @@ class HueBaseEntity(Entity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.device.id)},
)
# used for availability workaround
self._ignore_availability = None
self._last_state = None
@property
def name(self) -> str:
@@ -68,6 +71,7 @@ class HueBaseEntity(Entity):
async def async_added_to_hass(self) -> None:
"""Call when entity is added."""
self._check_availability_workaround()
# Add value_changed callbacks.
self.async_on_remove(
self.controller.subscribe(
@@ -98,11 +102,13 @@ class HueBaseEntity(Entity):
def available(self) -> bool:
"""Return entity availability."""
if self.device is None:
# devices without a device attached should be always available
# entities without a device attached should be always available
return True
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
# the zigbee connectivity sensor itself should be always available
return True
if self._ignore_availability:
return True
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
# all device-attached entities get availability from the zigbee connectivity
return zigbee.status == ConnectivityServiceStatus.CONNECTED
@@ -124,5 +130,53 @@ class HueBaseEntity(Entity):
ent_reg.async_remove(self.entity_id)
else:
self.logger.debug("Received status update for %s", self.entity_id)
self._check_availability_workaround()
self.on_update()
self.async_write_ha_state()
@callback
def _check_availability_workaround(self):
"""Check availability of the device."""
if self.resource.type != ResourceTypes.LIGHT:
return
if self._ignore_availability is not None:
# already processed
return
if self.device.product_data.certified:
# certified products report their state correctly
self._ignore_availability = False
# some (3th party) Hue lights report their connection status incorrectly
# causing the zigbee availability to report as disconnected while in fact
# it can be controlled. Although this is in fact something the device manufacturer
# should fix, we work around it here. If the light is reported unavailable
# by the zigbee connectivity but the state changes its considered as a
# malfunctioning device and we report it.
# while the user should actually fix this issue instead of ignoring it, we
# ignore the availability for this light from this point.
cur_state = self.resource.on.on
if self._last_state is None:
self._last_state = cur_state
return
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
if (
self._last_state != cur_state
and zigbee.status != ConnectivityServiceStatus.CONNECTED
):
# the device state changed from on->off or off->on
# while it was reported as not connected!
self.logger.warning(
"Light %s changed state while reported as disconnected. "
"This might be an indicator that routing is not working for this device. "
"Home Assistant will ignore availability for this light from now on. "
"Device details: %s - %s (%s) fw: %s",
self.name,
self.device.product_data.manufacturer_name,
self.device.product_data.product_name,
self.device.product_data.model_id,
self.device.product_data.software_version,
)
# do we want to store this in some persistent storage?
self._ignore_availability = True
else:
self._ignore_availability = False
self._last_state = cur_state
+40 -17
View File
@@ -6,16 +6,19 @@ from typing import Any
from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.groups import GroupedLight, Room, Zone
from aiohue.v2.models.feature import AlertEffectType
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_FLASH,
ATTR_TRANSITION,
ATTR_XY_COLOR,
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_ONOFF,
COLOR_MODE_XY,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
LightEntity,
)
@@ -24,14 +27,16 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from ..bridge import HueBridge
from ..const import DOMAIN
from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN
from .entity import HueBaseEntity
from .helpers import normalize_hue_brightness, normalize_hue_transition
ALLOWED_ERRORS = [
"device (groupedLight) has communication issues, command (on) may not have effect",
'device (groupedLight) is "soft off", command (on) may not have effect',
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"attribute (supportedAlertActions) cannot be written",
]
@@ -76,8 +81,6 @@ async def async_setup_entry(
class GroupedHueLight(HueBaseEntity, LightEntity):
"""Representation of a Grouped Hue light."""
# Entities for Hue groups are disabled by default
_attr_entity_registry_enabled_default = False
_attr_icon = "mdi:lightbulb-group"
def __init__(
@@ -90,8 +93,15 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
self.group = group
self.controller = controller
self.api: HueBridgeV2 = bridge.api
self._attr_supported_features |= SUPPORT_FLASH
self._attr_supported_features |= SUPPORT_TRANSITION
# Entities for Hue groups are disabled by default
# unless they were enabled in old version (legacy option)
self._attr_entity_registry_enabled_default = bridge.config_entry.data.get(
CONF_ALLOW_HUE_GROUPS, False
)
self._update_values()
async def async_added_to_hass(self) -> None:
@@ -138,16 +148,11 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
transition = kwargs.get(ATTR_TRANSITION)
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
xy_color = kwargs.get(ATTR_XY_COLOR)
color_temp = kwargs.get(ATTR_COLOR_TEMP)
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is not None:
# Hue uses a range of [0, 100] to control brightness.
brightness = float((brightness / 255) * 100)
if transition is not None:
# hue transition duration is in steps of 100 ms
transition = int(transition * 100)
brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
flash = kwargs.get(ATTR_FLASH)
# NOTE: a grouped_light can only handle turn on/off
# To set other features, you'll have to control the attached lights
@@ -156,6 +161,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
and xy_color is None
and color_temp is None
and transition is None
and flash is None
):
await self.bridge.async_request_call(
self.controller.set_state,
@@ -176,17 +182,34 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
color_xy=xy_color if light.supports_color else None,
color_temp=color_temp if light.supports_color_temperature else None,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=False,
allowed_errors=ALLOWED_ERRORS,
)
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
# NOTE: a grouped_light can only handle turn on/off
# To set other features, you'll have to control the attached lights
if transition is None:
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=False,
allowed_errors=ALLOWED_ERRORS,
)
return
# redirect all other feature commands to underlying lights
for light in self.controller.get_lights(self.resource.id):
await self.bridge.async_request_call(
self.api.lights.set_state,
light.id,
on=False,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
@callback
def on_update(self) -> None:
@@ -0,0 +1,19 @@
"""Helper functions for Philips Hue v2."""
def normalize_hue_brightness(brightness):
"""Return calculated brightness values."""
if brightness is not None:
# Hue uses a range of [0, 100] to control brightness.
brightness = float((brightness / 255) * 100)
return brightness
def normalize_hue_transition(transition):
"""Return rounded transition values."""
if transition is not None:
# hue transition duration is in milliseconds and round them to 100ms
transition = int(round(transition, 1) * 1000)
return transition
+22 -2
View File
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.models.button import Button
from aiohue.v2.models.button import Button, ButtonEvent
from homeassistant.const import CONF_DEVICE_ID, CONF_ID, CONF_TYPE, CONF_UNIQUE_ID
from homeassistant.core import callback
@@ -27,6 +27,11 @@ async def async_setup_hue_events(bridge: "HueBridge"):
api: HueBridgeV2 = bridge.api # to satisfy typing
conf_entry = bridge.config_entry
dev_reg = device_registry.async_get(hass)
last_state = {
x.id: x.button.last_event
for x in api.sensors.button.items
if x.button is not None
}
# at this time the `button` resource is the only source of hue events
btn_controller = api.sensors.button
@@ -35,6 +40,21 @@ async def async_setup_hue_events(bridge: "HueBridge"):
def handle_button_event(evt_type: EventType, hue_resource: Button) -> None:
"""Handle event from Hue devices controller."""
LOGGER.debug("Received button event: %s", hue_resource)
# guard for missing button object on the resource
if hue_resource.button is None:
return
cur_event = hue_resource.button.last_event
last_event = last_state.get(hue_resource.id)
# ignore the event if the last_event value is exactly the same
# this may happen if some other metadata of the button resource is adjusted
if cur_event == last_event:
return
if cur_event != ButtonEvent.REPEAT:
# do not store repeat event
last_state[hue_resource.id] = cur_event
hue_device = btn_controller.get_device(hue_resource.id)
device = dev_reg.async_get_device({(DOMAIN, hue_device.id)})
@@ -44,7 +64,7 @@ async def async_setup_hue_events(bridge: "HueBridge"):
CONF_ID: slugify(f"{hue_device.metadata.name}: Button"),
CONF_DEVICE_ID: device.id, # type: ignore
CONF_UNIQUE_ID: hue_resource.id,
CONF_TYPE: hue_resource.button.last_event.value,
CONF_TYPE: cur_event.value,
CONF_SUBTYPE: hue_resource.metadata.control_id,
}
hass.bus.async_fire(ATTR_HUE_EVENT, data)
+14 -12
View File
@@ -6,17 +6,20 @@ from typing import Any
from aiohue import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.lights import LightsController
from aiohue.v2.models.feature import AlertEffectType
from aiohue.v2.models.light import Light
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_FLASH,
ATTR_TRANSITION,
ATTR_XY_COLOR,
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_ONOFF,
COLOR_MODE_XY,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
LightEntity,
)
@@ -27,10 +30,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from ..bridge import HueBridge
from ..const import DOMAIN
from .entity import HueBaseEntity
from .helpers import normalize_hue_brightness, normalize_hue_transition
ALLOWED_ERRORS = [
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"attribute (supportedAlertActions) cannot be written",
]
@@ -68,6 +73,7 @@ class HueLight(HueBaseEntity, LightEntity):
) -> None:
"""Initialize the light."""
super().__init__(bridge, controller, resource)
self._attr_supported_features |= SUPPORT_FLASH
self.resource = resource
self.controller = controller
self._supported_color_modes = set()
@@ -150,16 +156,11 @@ class HueLight(HueBaseEntity, LightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
transition = kwargs.get(ATTR_TRANSITION)
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
xy_color = kwargs.get(ATTR_XY_COLOR)
color_temp = kwargs.get(ATTR_COLOR_TEMP)
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is not None:
# Hue uses a range of [0, 100] to control brightness.
brightness = float((brightness / 255) * 100)
if transition is not None:
# hue transition duration is in steps of 100 ms
transition = int(transition * 100)
brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
flash = kwargs.get(ATTR_FLASH)
await self.bridge.async_request_call(
self.controller.set_state,
@@ -169,19 +170,20 @@ class HueLight(HueBaseEntity, LightEntity):
color_xy=xy_color,
color_temp=color_temp,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
transition = kwargs.get(ATTR_TRANSITION)
if transition is not None:
# hue transition duration is in steps of 100 ms
transition = int(transition * 100)
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
flash = kwargs.get(ATTR_FLASH)
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=False,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)
@@ -2,7 +2,7 @@
"domain": "hunterdouglas_powerview",
"name": "Hunter Douglas PowerView",
"documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview",
"requirements": ["aiopvapi==1.6.14"],
"requirements": ["aiopvapi==1.6.19"],
"codeowners": ["@bdraco"],
"config_flow": true,
"homekit": {
+14 -5
View File
@@ -29,6 +29,7 @@ from homeassistant.const import (
CONF_PORT,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
@@ -44,6 +45,7 @@ from .const import (
CONF_KNX_INDIVIDUAL_ADDRESS,
CONF_KNX_ROUTING,
CONF_KNX_TUNNELING,
DATA_HASS_CONFIG,
DATA_KNX_CONFIG,
DOMAIN,
KNX_ADDRESS,
@@ -195,6 +197,7 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Start the KNX integration."""
hass.data[DATA_HASS_CONFIG] = config
conf: ConfigType | None = config.get(DOMAIN)
if conf is None:
@@ -251,15 +254,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
hass.config_entries.async_setup_platforms(
entry, [platform for platform in SUPPORTED_PLATFORMS if platform in config]
entry,
[
platform
for platform in SUPPORTED_PLATFORMS
if platform in config and platform is not Platform.NOTIFY
],
)
# set up notify platform, no entry support for notify component yet,
# have to use discovery to load platform.
if NotifySchema.PLATFORM in conf:
# set up notify platform, no entry support for notify component yet
if NotifySchema.PLATFORM in config:
hass.async_create_task(
discovery.async_load_platform(
hass, "notify", DOMAIN, conf[NotifySchema.PLATFORM], config
hass, "notify", DOMAIN, {}, hass.data[DATA_HASS_CONFIG]
)
)
@@ -312,6 +319,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
platform
for platform in SUPPORTED_PLATFORMS
if platform in hass.data[DATA_KNX_CONFIG]
and platform is not Platform.NOTIFY
],
)
if unload_ok:
@@ -383,6 +391,7 @@ class KNXModule:
if _conn_type == CONF_KNX_ROUTING:
return ConnectionConfig(
connection_type=ConnectionType.ROUTING,
local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP),
auto_reconnect=True,
)
if _conn_type == CONF_KNX_TUNNELING:
+17 -2
View File
@@ -137,9 +137,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
vol.Required(
ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False
): vol.Coerce(bool),
vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str,
}
if self.show_advanced_options:
fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str
return self.async_show_form(
step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors
)
@@ -195,6 +197,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONF_KNX_INDIVIDUAL_ADDRESS: user_input[
CONF_KNX_INDIVIDUAL_ADDRESS
],
ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get(
ConnectionSchema.CONF_KNX_LOCAL_IP
),
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
},
)
@@ -211,6 +216,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
): cv.port,
}
if self.show_advanced_options:
fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str
return self.async_show_form(
step_id="routing", data_schema=vol.Schema(fields), errors=errors
)
@@ -306,7 +314,6 @@ class KNXOptionsFlowHandler(OptionsFlow):
vol.Required(
CONF_PORT, default=self.current_config.get(CONF_PORT, 3671)
): cv.port,
vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str,
vol.Required(
ConnectionSchema.CONF_KNX_ROUTE_BACK,
default=self.current_config.get(
@@ -381,6 +388,14 @@ class KNXOptionsFlowHandler(OptionsFlow):
}
if self.show_advanced_options:
data_schema[
vol.Optional(
ConnectionSchema.CONF_KNX_LOCAL_IP,
default=self.current_config.get(
ConnectionSchema.CONF_KNX_LOCAL_IP,
),
)
] = str
data_schema[
vol.Required(
ConnectionSchema.CONF_KNX_STATE_UPDATER,
+3
View File
@@ -42,7 +42,10 @@ CONF_STATE_ADDRESS: Final = "state_address"
CONF_SYNC_STATE: Final = "sync_state"
CONF_KNX_INITIAL_CONNECTION_TYPES: Final = [CONF_KNX_TUNNELING, CONF_KNX_ROUTING]
# yaml config merged with config entry data
DATA_KNX_CONFIG: Final = "knx_config"
# original hass yaml config
DATA_HASS_CONFIG: Final = "knx_hass_config"
ATTR_COUNTER: Final = "counter"
ATTR_SOURCE: Final = "source"
+1 -1
View File
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/knx",
"requirements": [
"xknx==0.18.13"
"xknx==0.18.15"
],
"codeowners": [
"@Julius2342",
+19 -14
View File
@@ -11,7 +11,8 @@ from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN, KNX_ADDRESS
from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS
from .schema import NotifySchema
async def async_get_service(
@@ -20,24 +21,28 @@ async def async_get_service(
discovery_info: DiscoveryInfoType | None = None,
) -> KNXNotificationService | None:
"""Get the KNX notification service."""
if not discovery_info:
if discovery_info is None:
return None
platform_config: dict = discovery_info
xknx: XKNX = hass.data[DOMAIN].xknx
if platform_config := hass.data[DATA_KNX_CONFIG].get(NotifySchema.PLATFORM):
xknx: XKNX = hass.data[DOMAIN].xknx
notification_devices = []
for device_config in platform_config:
notification_devices.append(
XknxNotification(
xknx,
name=device_config[CONF_NAME],
group_address=device_config[KNX_ADDRESS],
notification_devices = []
for device_config in platform_config:
notification_devices.append(
XknxNotification(
xknx,
name=device_config[CONF_NAME],
group_address=device_config[KNX_ADDRESS],
)
)
return (
KNXNotificationService(notification_devices)
if notification_devices
else None
)
return (
KNXNotificationService(notification_devices) if notification_devices else None
)
return None
class KNXNotificationService(BaseNotificationService):
+4 -3
View File
@@ -28,7 +28,8 @@
"data": {
"individual_address": "Individual address for the routing connection",
"multicast_group": "The multicast group used for routing",
"multicast_port": "The multicast port used for routing"
"multicast_port": "The multicast port used for routing",
"local_ip": "Local IP (leave empty if unsure)"
}
}
},
@@ -48,6 +49,7 @@
"individual_address": "Default individual address",
"multicast_group": "Multicast group used for routing and discovery",
"multicast_port": "Multicast port used for routing and discovery",
"local_ip": "Local IP (leave empty if unsure)",
"state_updater": "Globally enable reading states from the KNX Bus",
"rate_limit": "Maximum outgoing telegrams per second"
}
@@ -56,8 +58,7 @@
"data": {
"port": "[%key:common::config_flow::data::port%]",
"host": "[%key:common::config_flow::data::host%]",
"route_back": "Route Back / NAT Mode",
"local_ip": "Local IP (leave empty if unsure)"
"route_back": "Route Back / NAT Mode"
}
}
}
@@ -22,7 +22,8 @@
"data": {
"individual_address": "Individual address for the routing connection",
"multicast_group": "The multicast group used for routing",
"multicast_port": "The multicast port used for routing"
"multicast_port": "The multicast port used for routing",
"local_ip": "Local IP (leave empty if unsure)"
},
"description": "Please configure the routing options."
},
@@ -48,6 +49,7 @@
"individual_address": "Default individual address",
"multicast_group": "Multicast group used for routing and discovery",
"multicast_port": "Multicast port used for routing and discovery",
"local_ip": "Local IP (leave empty if unsure)",
"rate_limit": "Maximum outgoing telegrams per second",
"state_updater": "Globally enable reading states from the KNX Bus"
}
@@ -55,7 +57,6 @@
"tunnel": {
"data": {
"host": "Host",
"local_ip": "Local IP (leave empty if unsure)",
"port": "Port",
"route_back": "Route Back / NAT Mode"
}
@@ -112,6 +112,7 @@ def log_entry(hass, name, message, domain=None, entity_id=None, context=None):
hass.add_job(async_log_entry, hass, name, message, domain, entity_id, context)
@callback
@bind_hass
def async_log_entry(hass, name, message, domain=None, entity_id=None, context=None):
"""Add an entry to the logbook."""
@@ -1,9 +1,8 @@
{
"disabled": "Library has incompatible requirements.",
"domain": "lupusec",
"name": "Lupus Electronics LUPUSEC",
"documentation": "https://www.home-assistant.io/integrations/lupusec",
"requirements": ["lupupy==0.0.21"],
"requirements": ["lupupy==0.0.24"],
"codeowners": ["@majuss"],
"iot_class": "local_polling"
}
+23 -4
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import timedelta
from http import HTTPStatus
import logging
from aiohttp.client_exceptions import ClientResponseError
@@ -30,7 +31,11 @@ from homeassistant.helpers.update_coordinator import (
UpdateFailed,
)
from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation
from .api import (
ConfigEntryLyricClient,
LyricLocalOAuth2Implementation,
OAuth2SessionLyric,
)
from .config_flow import OAuth2FlowHandler
from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
@@ -84,21 +89,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
session = aiohttp_client.async_get_clientsession(hass)
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
oauth_session = OAuth2SessionLyric(hass, entry, implementation)
client = ConfigEntryLyricClient(session, oauth_session)
client_id = hass.data[DOMAIN][CONF_CLIENT_ID]
lyric = Lyric(client, client_id)
async def async_update_data() -> Lyric:
async def async_update_data(force_refresh_token: bool = False) -> Lyric:
"""Fetch data from Lyric."""
await oauth_session.async_ensure_token_valid()
try:
if not force_refresh_token:
await oauth_session.async_ensure_token_valid()
else:
await oauth_session.force_refresh_token()
except ClientResponseError as exception:
if exception.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
raise ConfigEntryAuthFailed from exception
raise UpdateFailed(exception) from exception
try:
async with async_timeout.timeout(60):
await lyric.get_locations()
return lyric
except LyricAuthenticationException as exception:
# Attempt to refresh the token before failing.
# Honeywell appear to have issues keeping tokens saved.
_LOGGER.debug("Authentication failed. Attempting to refresh token")
if not force_refresh_token:
return await async_update_data(force_refresh_token=True)
raise ConfigEntryAuthFailed from exception
except (LyricException, ClientResponseError) as exception:
raise UpdateFailed(exception) from exception
+12
View File
@@ -8,6 +8,18 @@ from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
class OAuth2SessionLyric(config_entry_oauth2_flow.OAuth2Session):
"""OAuth2Session for Lyric."""
async def force_refresh_token(self) -> None:
"""Force a token refresh."""
new_token = await self.implementation.async_refresh_token(self.token)
self.hass.config_entries.async_update_entry(
self.config_entry, data={**self.config_entry.data, "token": new_token}
)
class ConfigEntryLyricClient(LyricClient):
"""Provide Honeywell Lyric authentication tied to an OAuth2 based config entry."""
@@ -3,7 +3,7 @@
"name": "MELCloud",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/melcloud",
"requirements": ["pymelcloud==2.5.5"],
"requirements": ["pymelcloud==2.5.6"],
"codeowners": ["@vilppuvuorinen"],
"iot_class": "cloud_polling"
}
+1 -1
View File
@@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["ffmpeg", "http", "media_source"],
"documentation": "https://www.home-assistant.io/integrations/nest",
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.5"],
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.9"],
"codeowners": ["@allenporter"],
"quality_scale": "platinum",
"dhcp": [
@@ -24,7 +24,7 @@ import logging
from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait
from google_nest_sdm.device import Device
from google_nest_sdm.event import ImageEventBase
from google_nest_sdm.event import EventImageType, ImageEventBase
from homeassistant.components.media_player.const import (
MEDIA_CLASS_DIRECTORY,
@@ -63,6 +63,9 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
async def get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]:
"""Return a mapping of device id to eligible Nest event media devices."""
if DATA_SUBSCRIBER not in hass.data[DOMAIN]:
# Integration unloaded, or is legacy nest integration
return {}
subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
device_manager = await subscriber.async_get_device_manager()
device_registry = await hass.helpers.device_registry.async_get_registry()
@@ -253,7 +256,7 @@ def _browse_event(
event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"),
event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT),
),
can_play=True,
can_play=(event.event_image_type == EventImageType.CLIP_PREVIEW),
can_expand=False,
thumbnail=None,
children=[],
@@ -2,7 +2,7 @@
"domain": "netgear",
"name": "NETGEAR",
"documentation": "https://www.home-assistant.io/integrations/netgear",
"requirements": ["pynetgear==0.7.0"],
"requirements": ["pynetgear==0.8.0"],
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
"iot_class": "local_polling",
"config_flow": true,
+21 -3
View File
@@ -11,6 +11,7 @@ from nexia.const import (
SYSTEM_STATUS_IDLE,
UNIT_FAHRENHEIT,
)
from nexia.util import find_humidity_setpoint
import voluptuous as vol
from homeassistant.components.climate import ClimateEntity
@@ -58,6 +59,8 @@ from .coordinator import NexiaDataUpdateCoordinator
from .entity import NexiaThermostatZoneEntity
from .util import percent_conv
PARALLEL_UPDATES = 1 # keep data in sync with only one connection at a time
SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode"
SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint"
SERVICE_SET_HVAC_RUN_MODE = "set_hvac_run_mode"
@@ -231,9 +234,9 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
def set_humidity(self, humidity):
"""Dehumidify target."""
if self._thermostat.has_dehumidify_support():
self._thermostat.set_dehumidify_setpoint(humidity / 100.0)
self.set_dehumidify_setpoint(humidity)
else:
self._thermostat.set_humidify_setpoint(humidity / 100.0)
self.set_humidify_setpoint(humidity)
self._signal_thermostat_update()
@property
@@ -453,7 +456,22 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
def set_humidify_setpoint(self, humidity):
"""Set the humidify setpoint."""
self._thermostat.set_humidify_setpoint(humidity / 100.0)
target_humidity = find_humidity_setpoint(humidity / 100.0)
if self._thermostat.get_humidify_setpoint() == target_humidity:
# Trying to set the humidify setpoint to the
# same value will cause the api to timeout
return
self._thermostat.set_humidify_setpoint(target_humidity)
self._signal_thermostat_update()
def set_dehumidify_setpoint(self, humidity):
"""Set the dehumidify setpoint."""
target_humidity = find_humidity_setpoint(humidity / 100.0)
if self._thermostat.get_dehumidify_setpoint() == target_humidity:
# Trying to set the dehumidify setpoint to the
# same value will cause the api to timeout
return
self._thermostat.set_dehumidify_setpoint(target_humidity)
self._signal_thermostat_update()
def _signal_thermostat_update(self):
+1 -1
View File
@@ -1,7 +1,7 @@
{
"domain": "nexia",
"name": "Nexia/American Standard/Trane",
"requirements": ["nexia==0.9.11"],
"requirements": ["nexia==0.9.12"],
"codeowners": ["@bdraco"],
"documentation": "https://www.home-assistant.io/integrations/nexia",
"config_flow": true,
+1 -1
View File
@@ -214,7 +214,7 @@ class NextBusDepartureSensor(SensorEntity):
# Generate list of upcoming times
self._attributes["upcoming"] = ", ".join(
sorted(p["minutes"] for p in predictions)
sorted((p["minutes"] for p in predictions), key=int)
)
latest_prediction = maybe_first(predictions)
+6 -1
View File
@@ -1,6 +1,8 @@
"""The 1-Wire component."""
import logging
from pyownet import protocol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
@@ -18,7 +20,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
onewirehub = OneWireHub(hass)
try:
await onewirehub.initialize(entry)
except CannotConnect as exc:
except (
CannotConnect, # Failed to connect to the server
protocol.OwnetError, # Connected to server, but failed to list the devices
) as exc:
raise ConfigEntryNotReady() from exc
hass.data[DOMAIN][entry.entry_id] = onewirehub
@@ -1,12 +1,15 @@
"""The pvpc_hourly_pricing integration to collect Spain official electric prices."""
from datetime import datetime, timedelta
import logging
from typing import Mapping
from aiopvpc import DEFAULT_POWER_KW, TARIFFS
from aiopvpc import DEFAULT_POWER_KW, TARIFFS, PVPCData
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_registry import (
EntityRegistry,
@@ -14,6 +17,8 @@ from homeassistant.helpers.entity_registry import (
async_migrate_entries,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import (
ATTR_POWER,
@@ -99,6 +104,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_remove(entry.entry_id)
return False
coordinator = ElecPricesDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_update_options))
return True
@@ -119,4 +128,39 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[Mapping[datetime, float]]):
"""Class to manage fetching Electricity prices data from API."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize."""
self.api = PVPCData(
session=async_get_clientsession(hass),
tariff=entry.data[ATTR_TARIFF],
local_timezone=hass.config.time_zone,
power=entry.data[ATTR_POWER],
power_valley=entry.data[ATTR_POWER_P3],
)
super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30)
)
self._entry = entry
@property
def entry_id(self) -> str:
"""Return entry ID."""
return self._entry.entry_id
async def _async_update_data(self) -> Mapping[datetime, float]:
"""Update electricity prices from the ESIOS API."""
prices = await self.api.async_update_prices(dt_util.utcnow())
self.api.process_state_and_attributes(dt_util.utcnow())
if not prices:
raise UpdateFailed
return prices
@@ -3,7 +3,7 @@
"name": "Spain electricity hourly pricing (PVPC)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing",
"requirements": ["aiopvpc==2.2.4"],
"requirements": ["aiopvpc==3.0.0"],
"codeowners": ["@azogue"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"
@@ -1,73 +1,160 @@
"""Sensor to collect the reference daily prices of electricity ('PVPC') in Spain."""
from __future__ import annotations
from collections.abc import Mapping
from datetime import datetime
import logging
from random import randint
from typing import Any
from aiopvpc import PVPCData
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CURRENCY_EURO, ENERGY_KILO_WATT_HOUR
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later, async_track_time_change
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import async_track_time_change
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF
from . import ElecPricesDataUpdateCoordinator
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
ATTR_PRICE = "price"
ICON = "mdi:currency-eur"
UNIT = f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}"
_DEFAULT_TIMEOUT = 10
PARALLEL_UPDATES = 1
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="PVPC",
icon="mdi:currency-eur",
native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}",
state_class=SensorStateClass.MEASUREMENT,
),
)
_PRICE_SENSOR_ATTRIBUTES_MAP = {
"tariff": "tariff",
"period": "period",
"available_power": "available_power",
"next_period": "next_period",
"hours_to_next_period": "hours_to_next_period",
"next_better_price": "next_better_price",
"hours_to_better_price": "hours_to_better_price",
"num_better_prices_ahead": "num_better_prices_ahead",
"price_position": "price_position",
"price_ratio": "price_ratio",
"max_price": "max_price",
"max_price_at": "max_price_at",
"min_price": "min_price",
"min_price_at": "min_price_at",
"next_best_at": "next_best_at",
"price_00h": "price_00h",
"price_01h": "price_01h",
"price_02h": "price_02h",
"price_02h_d": "price_02h_d", # only on DST day change with 25h
"price_03h": "price_03h",
"price_04h": "price_04h",
"price_05h": "price_05h",
"price_06h": "price_06h",
"price_07h": "price_07h",
"price_08h": "price_08h",
"price_09h": "price_09h",
"price_10h": "price_10h",
"price_11h": "price_11h",
"price_12h": "price_12h",
"price_13h": "price_13h",
"price_14h": "price_14h",
"price_15h": "price_15h",
"price_16h": "price_16h",
"price_17h": "price_17h",
"price_18h": "price_18h",
"price_19h": "price_19h",
"price_20h": "price_20h",
"price_21h": "price_21h",
"price_22h": "price_22h",
"price_23h": "price_23h",
# only seen in the evening
"next_better_price (next day)": "next_better_price (next day)",
"hours_to_better_price (next day)": "hours_to_better_price (next day)",
"num_better_prices_ahead (next day)": "num_better_prices_ahead (next day)",
"price_position (next day)": "price_position (next day)",
"price_ratio (next day)": "price_ratio (next day)",
"max_price (next day)": "max_price (next day)",
"max_price_at (next day)": "max_price_at (next day)",
"min_price (next day)": "min_price (next day)",
"min_price_at (next day)": "min_price_at (next day)",
"next_best_at (next day)": "next_best_at (next day)",
"price_next_day_00h": "price_next_day_00h",
"price_next_day_01h": "price_next_day_01h",
"price_next_day_02h": "price_next_day_02h",
"price_next_day_02h_d": "price_next_day_02h_d",
"price_next_day_03h": "price_next_day_03h",
"price_next_day_04h": "price_next_day_04h",
"price_next_day_05h": "price_next_day_05h",
"price_next_day_06h": "price_next_day_06h",
"price_next_day_07h": "price_next_day_07h",
"price_next_day_08h": "price_next_day_08h",
"price_next_day_09h": "price_next_day_09h",
"price_next_day_10h": "price_next_day_10h",
"price_next_day_11h": "price_next_day_11h",
"price_next_day_12h": "price_next_day_12h",
"price_next_day_13h": "price_next_day_13h",
"price_next_day_14h": "price_next_day_14h",
"price_next_day_15h": "price_next_day_15h",
"price_next_day_16h": "price_next_day_16h",
"price_next_day_17h": "price_next_day_17h",
"price_next_day_18h": "price_next_day_18h",
"price_next_day_19h": "price_next_day_19h",
"price_next_day_20h": "price_next_day_20h",
"price_next_day_21h": "price_next_day_21h",
"price_next_day_22h": "price_next_day_22h",
"price_next_day_23h": "price_next_day_23h",
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the electricity price sensor from config_entry."""
name = config_entry.data[CONF_NAME]
pvpc_data_handler = PVPCData(
tariff=config_entry.data[ATTR_TARIFF],
power=config_entry.data[ATTR_POWER],
power_valley=config_entry.data[ATTR_POWER_P3],
local_timezone=hass.config.time_zone,
websession=async_get_clientsession(hass),
timeout=_DEFAULT_TIMEOUT,
)
coordinator: ElecPricesDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
name = entry.data[CONF_NAME]
async_add_entities(
[ElecPriceSensor(name, config_entry.unique_id, pvpc_data_handler)], False
[ElecPriceSensor(coordinator, SENSOR_TYPES[0], entry.unique_id, name)]
)
class ElecPriceSensor(RestoreEntity, SensorEntity):
class ElecPriceSensor(CoordinatorEntity, SensorEntity):
"""Class to hold the prices of electricity as a sensor."""
_attr_icon = ICON
_attr_native_unit_of_measurement = UNIT
_attr_should_poll = False
_attr_state_class = STATE_CLASS_MEASUREMENT
coordinator: ElecPricesDataUpdateCoordinator
def __init__(self, name, unique_id, pvpc_data_handler):
"""Initialize the sensor object."""
self._name = name
self._unique_id = unique_id
self._pvpc_data = pvpc_data_handler
self._num_retries = 0
def __init__(
self,
coordinator: ElecPricesDataUpdateCoordinator,
description: SensorEntityDescription,
unique_id: str | None,
name: str,
) -> None:
"""Initialize ESIOS sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_attribution = coordinator.api.attribution
self._attr_unique_id = unique_id
self._attr_name = name
self._attr_device_info = DeviceInfo(
configuration_url="https://www.ree.es/es/apidatos",
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, coordinator.entry_id)},
manufacturer="REE",
name="PVPC (REData API)",
)
self._state: StateType = None
self._attrs: Mapping[str, Any] = {}
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
if state := await self.async_get_last_state():
self._pvpc_data.state = state.state
# Update 'state' value in hour changes
self.async_on_remove(
@@ -75,86 +162,31 @@ class ElecPriceSensor(RestoreEntity, SensorEntity):
self.hass, self.update_current_price, second=[0], minute=[0]
)
)
# Update prices at random time, 2 times/hour (don't want to upset API)
random_minute = randint(1, 29)
mins_update = [random_minute, random_minute + 30]
self.async_on_remove(
async_track_time_change(
self.hass, self.async_update_prices, second=[0], minute=mins_update
)
)
_LOGGER.debug(
"Setup of price sensor %s (%s) with tariff '%s', "
"updating prices each hour at %s min",
"Setup of price sensor %s (%s) with tariff '%s'",
self.name,
self.entity_id,
self._pvpc_data.tariff,
mins_update,
self.coordinator.api.tariff,
)
now = dt_util.utcnow()
await self.async_update_prices(now)
self.update_current_price(now)
@property
def unique_id(self) -> str | None:
"""Return a unique ID."""
return self._unique_id
@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
def native_value(self) -> float:
"""Return the state of the sensor."""
return self._pvpc_data.state
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._pvpc_data.state_available
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return self._pvpc_data.attributes
@callback
def update_current_price(self, now):
def update_current_price(self, now: datetime) -> None:
"""Update the sensor state, by selecting the current price for this hour."""
self._pvpc_data.process_state_and_attributes(now)
self.coordinator.api.process_state_and_attributes(now)
self.async_write_ha_state()
async def async_update_prices(self, now):
"""Update electricity prices from the ESIOS API."""
prices = await self._pvpc_data.async_update_prices(now)
if not prices and self._pvpc_data.source_available:
self._num_retries += 1
if self._num_retries > 2:
_LOGGER.warning(
"%s: repeated bad data update, mark component as unavailable source",
self.entity_id,
)
self._pvpc_data.source_available = False
return
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
self._state = self.coordinator.api.state
return self._state
retry_delay = 2 * self._num_retries * self._pvpc_data.timeout
_LOGGER.debug(
"%s: Bad update[retry:%d], will try again in %d s",
self.entity_id,
self._num_retries,
retry_delay,
)
async_call_later(self.hass, retry_delay, self.async_update_prices)
return
if not prices:
_LOGGER.debug("%s: data source is not yet available", self.entity_id)
return
self._num_retries = 0
if not self._pvpc_data.source_available:
self._pvpc_data.source_available = True
_LOGGER.warning("%s: component has recovered data access", self.entity_id)
self.update_current_price(now)
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the state attributes."""
self._attrs = {
_PRICE_SENSOR_ATTRIBUTES_MAP[key]: value
for key, value in self.coordinator.api.attributes.items()
if key in _PRICE_SENSOR_ATTRIBUTES_MAP
}
return self._attrs
@@ -85,7 +85,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
):
await self.async_set_unique_id(controller.mac)
self._abort_if_unique_id_configured(
updates={CONF_IP_ADDRESS: ip_address}
updates={CONF_IP_ADDRESS: ip_address}, reload_on_update=False
)
# A new rain machine: We will change out the unique id
@@ -5,7 +5,7 @@ from dataclasses import dataclass
from datetime import timedelta
import logging
import pyrepetier
import pyrepetierng as pyrepetier
import voluptuous as vol
from homeassistant.components.sensor import SensorEntityDescription
@@ -2,7 +2,7 @@
"domain": "repetier",
"name": "Repetier-Server",
"documentation": "https://www.home-assistant.io/integrations/repetier",
"requirements": ["pyrepetier==3.0.5"],
"codeowners": ["@MTrab"],
"requirements": ["pyrepetierng==0.1.0"],
"codeowners": ["@MTrab", "@ShadowBr0ther"],
"iot_class": "local_polling"
}
@@ -12,6 +12,7 @@ from homeassistant.const import (
CONF_FORCE_UPDATE,
CONF_NAME,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.event as evt
@@ -81,6 +82,7 @@ class RflinkBinarySensor(RflinkDevice, BinarySensorEntity):
if self._state and self._off_delay is not None:
@callback
def off_delay_listener(now):
"""Switch device off after a delay."""
self._delay_listener = None
+1 -1
View File
@@ -2,7 +2,7 @@
"domain": "ring",
"name": "Ring",
"documentation": "https://www.home-assistant.io/integrations/ring",
"requirements": ["ring_doorbell==0.7.1"],
"requirements": ["ring_doorbell==0.7.2"],
"dependencies": ["ffmpeg"],
"codeowners": ["@balloob"],
"config_flow": true,
+1 -2
View File
@@ -68,13 +68,12 @@ from .utils import (
BLOCK_PLATFORMS: Final = [
"binary_sensor",
"button",
"climate",
"cover",
"light",
"sensor",
"switch",
]
BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"]
BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "climate", "sensor"]
RPC_PLATFORMS: Final = ["binary_sensor", "button", "light", "sensor", "switch"]
_LOGGER: Final = logging.getLogger(__name__)
+171 -27
View File
@@ -3,12 +3,13 @@ from __future__ import annotations
import asyncio
import logging
from types import MappingProxyType
from typing import Any, Final, cast
from aioshelly.block_device import Block
import async_timeout
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity
from homeassistant.components.climate.const import (
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
@@ -20,11 +21,12 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.components.shelly import BlockDeviceWrapper
from homeassistant.components.shelly.entity import ShellyBlockEntity
from homeassistant.components.shelly.utils import get_device_entry_gen
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import device_registry, entity, entity_registry
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
@@ -49,10 +51,29 @@ async def async_setup_entry(
if get_device_entry_gen(config_entry) == 2:
return
wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
][BLOCK]
if wrapper.device.initialized:
await async_setup_climate_entities(async_add_entities, wrapper)
else:
await async_restore_climate_entities(
hass, config_entry, async_add_entities, wrapper
)
async def async_setup_climate_entities(
async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper,
) -> None:
"""Set up online climate devices."""
device_block: Block | None = None
sensor_block: Block | None = None
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK]
assert wrapper.device.blocks
for block in wrapper.device.blocks:
if block.type == "device":
device_block = block
@@ -60,10 +81,38 @@ async def async_setup_entry(
sensor_block = block
if sensor_block and device_block:
async_add_entities([ShellyClimate(wrapper, sensor_block, device_block)])
_LOGGER.debug("Setup online climate device %s", wrapper.name)
async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)])
class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
async def async_restore_climate_entities(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper,
) -> None:
"""Restore sleeping climate devices."""
ent_reg = await entity_registry.async_get_registry(hass)
entries = entity_registry.async_entries_for_config_entry(
ent_reg, config_entry.entry_id
)
for entry in entries:
if entry.domain != CLIMATE_DOMAIN:
continue
_LOGGER.debug("Setup sleeping climate device %s", wrapper.name)
_LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain)
async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)])
class BlockSleepingClimate(
RestoreEntity,
ClimateEntity,
entity.Entity,
):
"""Representation of a Shelly climate device."""
_attr_hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_HEAT]
@@ -74,45 +123,77 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
_attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"]
_attr_temperature_unit = TEMP_CELSIUS
# pylint: disable=super-init-not-called
def __init__(
self, wrapper: BlockDeviceWrapper, sensor_block: Block, device_block: Block
self,
wrapper: BlockDeviceWrapper,
sensor_block: Block | None,
device_block: Block | None,
entry: entity_registry.RegistryEntry | None = None,
) -> None:
"""Initialize climate."""
super().__init__(wrapper, sensor_block)
self.device_block = device_block
assert self.block.channel
self.wrapper = wrapper
self.block: Block | None = sensor_block
self.control_result: dict[str, Any] | None = None
self.device_block: Block | None = device_block
self.last_state: State | None = None
self.last_state_attributes: MappingProxyType[str, Any]
self._preset_modes: list[str] = []
self._attr_name = self.wrapper.name
self._attr_unique_id = self.wrapper.mac
self._attr_preset_modes: list[str] = [
PRESET_NONE,
*wrapper.device.settings["thermostats"][int(self.block.channel)][
"schedule_profile_names"
],
]
if self.block is not None and self.device_block is not None:
self._unique_id = f"{self.wrapper.mac}-{self.block.description}"
assert self.block.channel
self._preset_modes = [
PRESET_NONE,
*wrapper.device.settings["thermostats"][int(self.block.channel)][
"schedule_profile_names"
],
]
elif entry is not None:
self._unique_id = entry.unique_id
@property
def unique_id(self) -> str:
"""Set unique id of entity."""
return self._unique_id
@property
def name(self) -> str:
"""Name of entity."""
return self.wrapper.name
@property
def should_poll(self) -> bool:
"""If device should be polled."""
return False
@property
def target_temperature(self) -> float | None:
"""Set target temperature."""
return cast(float, self.block.targetTemp)
if self.block is not None:
return cast(float, self.block.targetTemp)
return self.last_state_attributes.get("temperature")
@property
def current_temperature(self) -> float | None:
"""Return current temperature."""
return cast(float, self.block.temp)
if self.block is not None:
return cast(float, self.block.temp)
return self.last_state_attributes.get("current_temperature")
@property
def available(self) -> bool:
"""Device availability."""
return not cast(bool, self.device_block.valveError)
if self.device_block is not None:
return not cast(bool, self.device_block.valveError)
return self.wrapper.last_update_success
@property
def hvac_mode(self) -> str:
"""HVAC current mode."""
if self.device_block is None:
return self.last_state.state if self.last_state else HVAC_MODE_OFF
if self.device_block.mode is None or self._check_is_off():
return HVAC_MODE_OFF
@@ -121,20 +202,45 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
@property
def preset_mode(self) -> str | None:
"""Preset current mode."""
if self.device_block is None:
return self.last_state_attributes.get("preset_mode")
if self.device_block.mode is None:
return None
return self._attr_preset_modes[cast(int, self.device_block.mode)]
return PRESET_NONE
return self._preset_modes[cast(int, self.device_block.mode)]
@property
def hvac_action(self) -> str | None:
"""HVAC current action."""
if self.device_block.status is None or self._check_is_off():
if (
self.device_block is None
or self.device_block.status is None
or self._check_is_off()
):
return CURRENT_HVAC_OFF
return (
CURRENT_HVAC_IDLE if self.device_block.status == "0" else CURRENT_HVAC_HEAT
)
@property
def preset_modes(self) -> list[str]:
"""Preset available modes."""
return self._preset_modes
@property
def device_info(self) -> DeviceInfo:
"""Device info."""
return {
"connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)}
}
@property
def channel(self) -> str | None:
"""Device channel."""
if self.block is not None:
return self.block.channel
return self.last_state_attributes.get("channel")
def _check_is_off(self) -> bool:
"""Return if valve is off or on."""
return bool(
@@ -148,7 +254,7 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return await self.wrapper.device.http_request(
"get", f"thermostat/{self.block.channel}", kwargs
"get", f"thermostat/{self.channel}", kwargs
)
except (asyncio.TimeoutError, OSError) as err:
_LOGGER.error(
@@ -186,3 +292,41 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
await self.set_state_full_path(
schedule=1, schedule_profile=f"{preset_index}"
)
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
_LOGGER.info("Restoring entity %s", self.name)
last_state = await self.async_get_last_state()
if last_state is not None:
self.last_state = last_state
self.last_state_attributes = self.last_state.attributes
self._preset_modes = cast(
list, self.last_state.attributes.get("preset_modes")
)
self.async_on_remove(self.wrapper.async_add_listener(self._update_callback))
async def async_update(self) -> None:
"""Update entity with latest info."""
await self.wrapper.async_request_refresh()
@callback
def _update_callback(self) -> None:
"""Handle device update."""
if not self.wrapper.device.initialized:
self.async_write_ha_state()
return
assert self.wrapper.device.blocks
for block in self.wrapper.device.blocks:
if block.type == "device":
self.device_block = block
if hasattr(block, "targetTemp"):
self.block = block
_LOGGER.debug("Entity %s attached to block", self.name)
self.async_write_ha_state()
return
@@ -12,6 +12,7 @@ from simplipy.errors import (
EndpointUnavailableError,
InvalidCredentialsError,
SimplipyError,
WebsocketError,
)
from simplipy.system import SystemNotification
from simplipy.system.v3 import (
@@ -472,6 +473,7 @@ class SimpliSafe:
self._api = api
self._hass = hass
self._system_notifications: dict[int, set[SystemNotification]] = {}
self._websocket_reconnect_task: asyncio.Task | None = None
self.entry = entry
self.initial_event_to_use: dict[int, dict[str, Any]] = {}
self.systems: dict[int, SystemType] = {}
@@ -516,11 +518,44 @@ class SimpliSafe:
self._system_notifications[system.system_id] = latest_notifications
async def _async_websocket_on_connect(self) -> None:
"""Define a callback for connecting to the websocket."""
async def _async_start_websocket_loop(self) -> None:
"""Start a websocket reconnection loop."""
if TYPE_CHECKING:
assert self._api.websocket
await self._api.websocket.async_listen()
should_reconnect = True
try:
await self._api.websocket.async_connect()
await self._api.websocket.async_listen()
except asyncio.CancelledError:
LOGGER.debug("Request to cancel websocket loop received")
raise
except WebsocketError as err:
LOGGER.error("Failed to connect to websocket: %s", err)
except Exception as err: # pylint: disable=broad-except
LOGGER.error("Unknown exception while connecting to websocket: %s", err)
if should_reconnect:
LOGGER.info("Disconnected from websocket; reconnecting")
await self._async_cancel_websocket_loop()
self._websocket_reconnect_task = self._hass.async_create_task(
self._async_start_websocket_loop()
)
async def _async_cancel_websocket_loop(self) -> None:
"""Stop any existing websocket reconnection loop."""
if self._websocket_reconnect_task:
self._websocket_reconnect_task.cancel()
try:
await self._websocket_reconnect_task
except asyncio.CancelledError:
LOGGER.debug("Websocket reconnection task successfully canceled")
self._websocket_reconnect_task = None
if TYPE_CHECKING:
assert self._api.websocket
await self._api.websocket.async_disconnect()
@callback
def _async_websocket_on_event(self, event: WebsocketEvent) -> None:
@@ -560,17 +595,17 @@ class SimpliSafe:
assert self._api.refresh_token
assert self._api.websocket
self._api.websocket.add_connect_callback(self._async_websocket_on_connect)
self._api.websocket.add_event_callback(self._async_websocket_on_event)
asyncio.create_task(self._api.websocket.async_connect())
self._websocket_reconnect_task = asyncio.create_task(
self._async_start_websocket_loop()
)
async def async_websocket_disconnect_listener(_: Event) -> None:
"""Define an event handler to disconnect from the websocket."""
if TYPE_CHECKING:
assert self._api.websocket
if self._api.websocket.connected:
await self._api.websocket.async_disconnect()
await self._async_cancel_websocket_loop()
self.entry.async_on_unload(
self._hass.bus.async_listen_once(
@@ -612,10 +647,24 @@ class SimpliSafe:
data={**self.entry.data, CONF_TOKEN: token},
)
async def async_handle_refresh_token(token: str) -> None:
"""Handle a new refresh token."""
async_save_refresh_token(token)
if TYPE_CHECKING:
assert self._api.websocket
# Open a new websocket connection with the fresh token:
await self._async_cancel_websocket_loop()
self._websocket_reconnect_task = self._hass.async_create_task(
self._async_start_websocket_loop()
)
self.entry.async_on_unload(
self._api.add_refresh_token_callback(async_save_refresh_token)
self._api.add_refresh_token_callback(async_handle_refresh_token)
)
# Save the refresh token we got on entry setup:
async_save_refresh_token(self._api.refresh_token)
async def async_update(self) -> None:
@@ -3,7 +3,7 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==2021.12.1"],
"requirements": ["simplisafe-python==2021.12.2"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling",
"dhcp": [
@@ -5,7 +5,7 @@
"documentation": "https://www.home-assistant.io/integrations/smappee",
"dependencies": ["http"],
"requirements": [
"pysmappee==0.2.27"
"pysmappee==0.2.29"
],
"codeowners": [
"@bsmappee"
+29 -34
View File
@@ -4,16 +4,11 @@ from __future__ import annotations
from dataclasses import dataclass
from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL,
SensorDeviceClass,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_VOLTAGE,
ELECTRIC_POTENTIAL_VOLT,
ENERGY_KILO_WATT_HOUR,
PERCENTAGE,
@@ -38,35 +33,35 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = (
SolarLogSensorEntityDescription(
key="time",
name="last update",
device_class=DEVICE_CLASS_TIMESTAMP,
device_class=SensorDeviceClass.TIMESTAMP,
),
SolarLogSensorEntityDescription(
key="power_ac",
name="power AC",
icon="mdi:solar-power",
native_unit_of_measurement=POWER_WATT,
state_class=STATE_CLASS_MEASUREMENT,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="power_dc",
name="power DC",
icon="mdi:solar-power",
native_unit_of_measurement=POWER_WATT,
state_class=STATE_CLASS_MEASUREMENT,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="voltage_ac",
name="voltage AC",
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="voltage_dc",
name="voltage DC",
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="yield_day",
@@ -101,50 +96,50 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = (
name="yield total",
icon="mdi:solar-power",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
state_class=STATE_CLASS_TOTAL,
state_class=SensorStateClass.TOTAL,
factor=0.001,
),
SolarLogSensorEntityDescription(
key="consumption_ac",
name="consumption AC",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="consumption_day",
name="consumption day",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
device_class=SensorDeviceClass.ENERGY,
factor=0.001,
),
SolarLogSensorEntityDescription(
key="consumption_yesterday",
name="consumption yesterday",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
device_class=SensorDeviceClass.ENERGY,
factor=0.001,
),
SolarLogSensorEntityDescription(
key="consumption_month",
name="consumption month",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
device_class=SensorDeviceClass.ENERGY,
factor=0.001,
),
SolarLogSensorEntityDescription(
key="consumption_year",
name="consumption year",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
device_class=SensorDeviceClass.ENERGY,
factor=0.001,
),
SolarLogSensorEntityDescription(
key="consumption_total",
name="consumption total",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
factor=0.001,
),
SolarLogSensorEntityDescription(
@@ -152,31 +147,31 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = (
name="installed peak power",
icon="mdi:solar-power",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
device_class=SensorDeviceClass.POWER,
),
SolarLogSensorEntityDescription(
key="alternator_loss",
name="alternator loss",
icon="mdi:solar-power",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="capacity",
name="capacity",
icon="mdi:solar-power",
native_unit_of_measurement=PERCENTAGE,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
factor=100,
),
SolarLogSensorEntityDescription(
key="efficiency",
name="efficiency",
native_unit_of_measurement=PERCENTAGE,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
factor=100,
),
SolarLogSensorEntityDescription(
@@ -184,15 +179,15 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = (
name="power available",
icon="mdi:solar-power",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="usage",
name="usage",
native_unit_of_measurement=PERCENTAGE,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
factor=100,
),
)
+12 -6
View File
@@ -1,7 +1,8 @@
"""Platform for solarlog sensors."""
from homeassistant.components.sensor import SensorEntity
from homeassistant.helpers import update_coordinator
from homeassistant.helpers.entity import DeviceInfo, StateType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.util.dt import as_local
from . import SolarlogData
from .const import DOMAIN, SENSOR_TYPES, SolarLogSensorEntityDescription
@@ -38,11 +39,16 @@ class SolarlogSensor(update_coordinator.CoordinatorEntity, SensorEntity):
)
@property
def native_value(self) -> StateType:
def native_value(self):
"""Return the native sensor value."""
result = getattr(self.coordinator.data, self.entity_description.key)
if self.entity_description.factor:
state = round(result * self.entity_description.factor, 3)
if self.entity_description.key == "time":
state = as_local(
getattr(self.coordinator.data, self.entity_description.key)
)
else:
state = result
result = getattr(self.coordinator.data, self.entity_description.key)
if self.entity_description.factor:
state = round(result * self.entity_description.factor, 3)
else:
state = result
return state
@@ -1,6 +1,7 @@
"""Entity representing a Sonos power sensor."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.binary_sensor import (
@@ -16,11 +17,14 @@ from .speaker import SonosSpeaker
ATTR_BATTERY_POWER_SOURCE = "power_source"
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Sonos from a config entry."""
async def _async_create_entity(speaker: SonosSpeaker) -> None:
_LOGGER.debug("Creating battery binary_sensor on %s", speaker.zone_name)
entity = SonosPowerEntity(speaker)
async_add_entities([entity])
-1
View File
@@ -149,7 +149,6 @@ SONOS_CREATE_BATTERY = "sonos_create_battery"
SONOS_CREATE_SWITCHES = "sonos_create_switches"
SONOS_CREATE_LEVELS = "sonos_create_levels"
SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player"
SONOS_ENTITY_CREATED = "sonos_entity_created"
SONOS_POLL_UPDATE = "sonos_poll_update"
SONOS_ALARMS_UPDATED = "sonos_alarms_updated"
SONOS_FAVORITES_UPDATED = "sonos_favorites_updated"
+1 -8
View File
@@ -10,15 +10,11 @@ from soco.core import SoCo
from soco.exceptions import SoCoException
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import (
DOMAIN,
SONOS_ENTITY_CREATED,
SONOS_FAVORITES_UPDATED,
SONOS_POLL_UPDATE,
SONOS_STATE_UPDATED,
@@ -60,9 +56,6 @@ class SonosEntity(Entity):
self.async_write_ha_state,
)
)
async_dispatcher_send(
self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain
)
async def async_poll(self, now: datetime.datetime) -> None:
"""Poll the entity if subscriptions fail."""
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Sonos",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos",
"requirements": ["soco==0.25.0"],
"requirements": ["soco==0.25.1"],
"dependencies": ["ssdp"],
"after_dependencies": ["plex", "zeroconf"],
"zeroconf": ["_sonos._tcp.local."],
@@ -132,6 +132,7 @@ async def async_setup_entry(
@callback
def async_create_entities(speaker: SonosSpeaker) -> None:
"""Handle device discovery and create entities."""
_LOGGER.debug("Creating media_player on %s", speaker.zone_name)
async_add_entities([SonosMediaPlayerEntity(speaker)])
@service.verify_domain_control(hass, SONOS_DOMAIN)
+7
View File
@@ -1,6 +1,8 @@
"""Entity representing a Sonos number control."""
from __future__ import annotations
import logging
from homeassistant.components.number import NumberEntity
from homeassistant.const import ENTITY_CATEGORY_CONFIG
from homeassistant.core import callback
@@ -13,6 +15,8 @@ from .speaker import SonosSpeaker
LEVEL_TYPES = ("bass", "treble")
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Sonos number platform from a config entry."""
@@ -21,6 +25,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
def _async_create_entities(speaker: SonosSpeaker) -> None:
entities = []
for level_type in LEVEL_TYPES:
_LOGGER.debug(
"Creating %s number control on %s", level_type, speaker.zone_name
)
entities.append(SonosLevelEntity(speaker, level_type))
async_add_entities(entities)
+6
View File
@@ -1,6 +1,8 @@
"""Entity representing a Sonos battery level."""
from __future__ import annotations
import logging
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_BATTERY,
@@ -14,6 +16,8 @@ from .const import SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY
from .entity import SonosEntity
from .speaker import SonosSpeaker
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Sonos from a config entry."""
@@ -22,11 +26,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
def _async_create_audio_format_entity(
speaker: SonosSpeaker, audio_format: str
) -> None:
_LOGGER.debug("Creating audio input format sensor on %s", speaker.zone_name)
entity = SonosAudioInputFormatSensorEntity(speaker, audio_format)
async_add_entities([entity])
@callback
def _async_create_battery_sensor(speaker: SonosSpeaker) -> None:
_LOGGER.debug("Creating battery level sensor on %s", speaker.zone_name)
entity = SonosBatteryEntity(speaker)
async_add_entities([entity])
+18 -35
View File
@@ -20,10 +20,7 @@ from soco.music_library import MusicLibrary
from soco.plugins.sharelink import ShareLinkPlugin
from soco.snapshot import Snapshot
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -42,7 +39,6 @@ from .const import (
BATTERY_SCAN_INTERVAL,
DATA_SONOS,
DOMAIN,
PLATFORMS,
SCAN_INTERVAL,
SONOS_CHECK_ACTIVITY,
SONOS_CREATE_ALARM,
@@ -51,7 +47,6 @@ from .const import (
SONOS_CREATE_LEVELS,
SONOS_CREATE_MEDIA_PLAYER,
SONOS_CREATE_SWITCHES,
SONOS_ENTITY_CREATED,
SONOS_POLL_UPDATE,
SONOS_REBOOTED,
SONOS_SPEAKER_ACTIVITY,
@@ -161,9 +156,6 @@ class SonosSpeaker:
self._share_link_plugin: ShareLinkPlugin | None = None
self.available = True
# Synchronization helpers
self._platforms_ready: set[str] = set()
# Subscriptions and events
self.subscriptions_failed: bool = False
self._subscriptions: list[SubscriptionBase] = []
@@ -193,7 +185,7 @@ class SonosSpeaker:
self.volume: int | None = None
self.muted: bool | None = None
self.night_mode: bool | None = None
self.dialog_mode: bool | None = None
self.dialog_level: bool | None = None
self.cross_fade: bool | None = None
self.bass: int | None = None
self.treble: int | None = None
@@ -217,7 +209,6 @@ class SonosSpeaker:
dispatch_pairs = (
(SONOS_CHECK_ACTIVITY, self.async_check_activity),
(SONOS_SPEAKER_ADDED, self.update_group_for_uid),
(f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.async_handle_new_entity),
(f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted),
(f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", self.speaker_activity),
)
@@ -253,15 +244,11 @@ class SonosSpeaker:
self.hass, self.async_poll_battery, BATTERY_SCAN_INTERVAL
)
dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self)
else:
self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN})
if new_alarms := [
alarm.alarm_id for alarm in self.alarms if alarm.zone.uid == self.soco.uid
]:
dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms)
else:
self._platforms_ready.add(SWITCH_DOMAIN)
dispatcher_send(self.hass, SONOS_CREATE_SWITCHES, self)
@@ -277,19 +264,11 @@ class SonosSpeaker:
dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self)
dispatcher_send(self.hass, SONOS_SPEAKER_ADDED, self.soco.uid)
self.hass.create_task(self.async_subscribe())
#
# Entity management
#
async def async_handle_new_entity(self, entity_type: str) -> None:
"""Listen to new entities to trigger first subscription."""
if self._platforms_ready == PLATFORMS:
return
self._platforms_ready.add(entity_type)
if self._platforms_ready == PLATFORMS:
self._resubscription_lock = asyncio.Lock()
await self.async_subscribe()
def write_entity_states(self) -> None:
"""Write states for associated SonosEntity instances."""
dispatcher_send(self.hass, f"{SONOS_STATE_UPDATED}-{self.soco.uid}")
@@ -405,6 +384,9 @@ class SonosSpeaker:
async def async_resubscribe(self, exception: Exception) -> None:
"""Attempt to resubscribe when a renewal failure is detected."""
if not self._resubscription_lock:
self._resubscription_lock = asyncio.Lock()
async with self._resubscription_lock:
if not self.available:
return
@@ -498,17 +480,18 @@ class SonosSpeaker:
if "mute" in variables:
self.muted = variables["mute"]["Master"] == "1"
if "night_mode" in variables:
self.night_mode = variables["night_mode"] == "1"
for bool_var in (
"dialog_level",
"night_mode",
"sub_enabled",
"surround_enabled",
):
if bool_var in variables:
setattr(self, bool_var, variables[bool_var] == "1")
if "dialog_level" in variables:
self.dialog_mode = variables["dialog_level"] == "1"
if "bass" in variables:
self.bass = variables["bass"]
if "treble" in variables:
self.treble = variables["treble"]
for int_var in ("bass", "treble"):
if int_var in variables:
setattr(self, int_var, variables[int_var])
self.async_write_entity_states()
@@ -982,7 +965,7 @@ class SonosSpeaker:
self.volume = self.soco.volume
self.muted = self.soco.mute
self.night_mode = self.soco.night_mode
self.dialog_mode = self.soco.dialog_mode
self.dialog_level = self.soco.dialog_mode
self.bass = self.soco.bass
self.treble = self.soco.treble
+1 -1
View File
@@ -2,7 +2,7 @@
"domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/ssdp",
"requirements": ["async-upnp-client==0.22.12"],
"requirements": ["async-upnp-client==0.23.1"],
"dependencies": ["network"],
"after_dependencies": ["zeroconf"],
"codeowners": [],
@@ -3,7 +3,7 @@
"name": "Tailscale",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tailscale",
"requirements": ["tailscale==0.1.4"],
"requirements": ["tailscale==0.1.6"],
"codeowners": ["@frenck"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"
+5 -1
View File
@@ -641,9 +641,13 @@ class LightTemplate(TemplateEntity, LightEntity):
@callback
def _update_color(self, render):
"""Update the hs_color from the template."""
if render is None:
self._color = None
return
h_str = s_str = None
if isinstance(render, str):
if render in (None, "None", ""):
if render in ("None", ""):
self._color = None
return
h_str, s_str = map(
@@ -2,7 +2,7 @@
"domain": "tibber",
"name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber",
"requirements": ["pyTibber==0.21.0"],
"requirements": ["pyTibber==0.21.1"],
"codeowners": ["@danielhiversen"],
"quality_scale": "silver",
"config_flow": true,
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Tile",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tile",
"requirements": ["pytile==5.2.4"],
"requirements": ["pytile==2021.12.0"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling"
}
@@ -2,7 +2,7 @@
"domain": "totalconnect",
"name": "Total Connect",
"documentation": "https://www.home-assistant.io/integrations/totalconnect",
"requirements": ["total_connect_client==2021.11.4"],
"requirements": ["total_connect_client==2021.12"],
"dependencies": [],
"codeowners": ["@austinmroczek"],
"config_flow": true,
@@ -16,6 +16,7 @@ from homeassistant.const import (
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import get_time_zone
_LOGGER = logging.getLogger(__name__)
@@ -131,12 +132,15 @@ class TrainSensor(SensorEntity):
self._state = None
self._departure_state = None
self._delay_in_minutes = None
self._timezone = get_time_zone("Europe/Stockholm")
async def async_update(self):
"""Retrieve latest state."""
if self._time is not None:
departure_day = next_departuredate(self._weekday)
when = datetime.combine(departure_day, self._time)
when = datetime.combine(departure_day, self._time).astimezone(
self._timezone
)
try:
self._state = await self._train_api.async_get_train_stop(
self._from_station, self._to_station, when
@@ -193,8 +197,8 @@ class TrainSensor(SensorEntity):
"""Return the departure state."""
if (state := self._state) is not None:
if state.time_at_location is not None:
return state.time_at_location
return state.time_at_location.astimezone(self._timezone)
if state.estimated_time_at_location is not None:
return state.estimated_time_at_location
return state.advertised_time_at_location
return state.estimated_time_at_location.astimezone(self._timezone)
return state.advertised_time_at_location.astimezone(self._timezone)
return None
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "UPnP/IGD",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/upnp",
"requirements": ["async-upnp-client==0.22.12"],
"requirements": ["async-upnp-client==0.23.1"],
"dependencies": ["network", "ssdp"],
"codeowners": ["@StevenLooman","@ehendrix23"],
"ssdp": [
+21 -2
View File
@@ -1,6 +1,7 @@
"""Support for Vallox ventilation units."""
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
import ipaddress
import logging
@@ -13,7 +14,7 @@ from vallox_websocket_api.vallox import get_uuid as calculate_uuid
import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import CoreState, HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.typing import ConfigType, StateType
@@ -25,6 +26,7 @@ from .const import (
DEFAULT_FAN_SPEED_HOME,
DEFAULT_NAME,
DOMAIN,
INITIAL_COORDINATOR_UPDATE_RETRY_INTERVAL_SECONDS,
METRIC_KEY_PROFILE_FAN_SPEED_AWAY,
METRIC_KEY_PROFILE_FAN_SPEED_BOOST,
METRIC_KEY_PROFILE_FAN_SPEED_HOME,
@@ -171,7 +173,24 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.data[DOMAIN] = {"client": client, "coordinator": coordinator, "name": name}
async def _async_load_platform_delayed(*_: Any) -> None:
await coordinator.async_refresh()
# We need a successful update before loading the platforms, because platform init code
# derives the UUIDs from the data the coordinator fetches.
warned_once = False
while hass.state == CoreState.running:
await coordinator.async_refresh()
if coordinator.last_update_success:
break
if not warned_once:
_LOGGER.warning(
"Vallox integration not ready yet; Retrying in background"
)
warned_once = True
await asyncio.sleep(INITIAL_COORDINATOR_UPDATE_RETRY_INTERVAL_SECONDS)
else:
return
hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
hass.async_create_task(async_load_platform(hass, "fan", DOMAIN, {}, config))
+1
View File
@@ -7,6 +7,7 @@ from vallox_websocket_api import PROFILE as VALLOX_PROFILE
DOMAIN = "vallox"
DEFAULT_NAME = "Vallox"
INITIAL_COORDINATOR_UPDATE_RETRY_INTERVAL_SECONDS = 5
STATE_SCAN_INTERVAL = timedelta(seconds=60)
# Common metric keys and (default) values.
+2 -1
View File
@@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from . import ValloxDataUpdateCoordinator
from .const import (
@@ -107,7 +108,7 @@ class ValloxFilterRemainingSensor(ValloxSensor):
days_remaining_delta = timedelta(days=days_remaining)
now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0)
return now + days_remaining_delta
return (now + days_remaining_delta).astimezone(dt_util.UTC)
class ValloxCellStateSensor(ValloxSensor):
@@ -61,6 +61,11 @@ class VelbusClimate(VelbusEntity, ClimateEntity):
None,
)
@property
def current_temperature(self) -> int | None:
"""Return the current temperature."""
return self._channel.get_state()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperatures."""
if (temp := kwargs.get(ATTR_TEMPERATURE)) is None:
+2 -2
View File
@@ -49,7 +49,7 @@ class VelbusLight(VelbusEntity, LightEntity):
"""Representation of a Velbus light."""
_channel: VelbusDimmer
_attr_supported_feature = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
_attr_supported_features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
@property
def is_on(self) -> bool:
@@ -96,7 +96,7 @@ class VelbusButtonLight(VelbusEntity, LightEntity):
_channel: VelbusButton
_attr_entity_registry_enabled_default = False
_attr_supported_feature = SUPPORT_FLASH
_attr_supported_features = SUPPORT_FLASH
def __init__(self, channel: VelbusChannel) -> None:
"""Initialize the button light (led)."""
@@ -0,0 +1,26 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"user": {
"title": "{name}",
"description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"scan_interval": "Scan Interval (seconds)",
"username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]",
"client_id": "[%key:common::config_flow::data::api_key%]",
"heating_type": "Heating type"
}
}
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}
@@ -0,0 +1,26 @@
{
"config": {
"abort": {
"single_instance_allowed": "Already configured. Only a single configuration possible.",
"unknown": "Unexpected error"
},
"error": {
"invalid_auth": "Invalid authentication"
},
"flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
"name": "Name",
"scan_interval": "Scan Interval (seconds)",
"client_id": "API Key",
"heating_type": "Heating type",
"password": "Password",
"username": "Email"
},
"title": "{name}",
"description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com"
}
}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Belkin WeMo",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wemo",
"requirements": ["pywemo==0.6.7"],
"requirements": ["pywemo==0.7.0"],
"ssdp": [
{
"manufacturer": "Belkin International Inc."
@@ -3,7 +3,7 @@
"name": "Xiaomi Miio",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
"requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.1"],
"requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.2"],
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
"zeroconf": ["_miio._udp.local."],
"iot_class": "local_polling"
@@ -2,7 +2,7 @@
"domain": "yeelight",
"name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/yeelight",
"requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.12"],
"requirements": ["yeelight==0.7.8", "async-upnp-client==0.23.1"],
"codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
"config_flow": true,
"dependencies": ["network"],
+2 -2
View File
@@ -206,11 +206,11 @@ class Thermostat(ZhaEntity, ClimateEntity):
unoccupied_cooling_setpoint = self._thrm.unoccupied_cooling_setpoint
if unoccupied_cooling_setpoint is not None:
data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_cooling_setpoint
data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_cooling_setpoint
unoccupied_heating_setpoint = self._thrm.unoccupied_heating_setpoint
if unoccupied_heating_setpoint is not None:
data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_heating_setpoint
data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_heating_setpoint
return data
@property
@@ -234,6 +234,7 @@ class GroupProbe:
unsub()
self._unsubs.remove(unsub)
@callback
def _reprobe_group(self, group_id: int) -> None:
"""Reprobe a group for entities after its members change."""
zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY]

Some files were not shown because too many files have changed in this diff Show More