Compare commits

..

109 Commits

Author SHA1 Message Date
Paulus Schoutsen
e5c5790768 2023.6.2 (#94621) 2023-06-15 00:19:30 -04:00
Paulus Schoutsen
905bdd0dd5 Bumped version to 2023.6.2 2023-06-14 21:18:40 -04:00
Erik Montnemery
9cbcfca2cd Improve multipan debug logging (#94580) 2023-06-14 21:18:35 -04:00
Chris Talkington
e6b8e4fd48 Fix failed recovery in ipp (#94573) 2023-06-14 21:18:34 -04:00
Chris Talkington
8f437c5833 Fix failed recovery in roku (#94572) 2023-06-14 21:18:33 -04:00
Ian Foster
d28d909114 Fix keyboard_remote for python 3.11 (#94570)
* started work to update keyboard_remote to work with python 3.11

* updated function names

* all checks pass

* fixed asyncio for python 3.11

* cleanup

* Update homeassistant/components/keyboard_remote/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update __init__.py

added:
from __future__ import annotations

* Fix typing

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-06-14 21:18:32 -04:00
Maciej Bieniek
f67577ebe0 Catch InvalidAuthError in shutdown() method for Shelly gen2 devices (#94563)
* Catch InvalidAuthError in shutdown() method

* Add test

* Revert unwanted change in tests
2023-06-14 21:18:31 -04:00
Franck Nijhof
70d33129d4 Update Home Assistant base image to 2023.06.0 (#94556) 2023-06-14 21:18:29 -04:00
Aaron Bach
a63ce8100e Bump regenmaschine to 2023.06.0 (#94554) 2023-06-14 21:18:28 -04:00
J. Nick Koston
d557c6e43e Bump yalexs-ble to 2.1.18 (#94547) 2023-06-14 21:18:27 -04:00
Raman Gupta
fd0404bb4a Fix entity and device selector TypedDict's (#94510) 2023-06-14 21:18:26 -04:00
Chris Phillips
576cf52573 Bump russound_rio to 1.0.0 (#94500) 2023-06-14 21:17:51 -04:00
mover85
e83f0bb7a5 Revert "Bump pydaikin 2.9.1 (#93635)" (#94469)
Revert to pydaikin 2.9.0
2023-06-14 21:17:08 -04:00
Raman Gupta
fa8e952324 Set default value for endpoint in zwave device automations (#94445)
* Set default value for endpoint in zwave device automations

* add test case
2023-06-14 21:17:07 -04:00
G Johansson
25a4679266 Fix reload service in Command Line (#94436)
* Fix reload in Command Line

* Add read new yaml
2023-06-14 21:17:06 -04:00
G Johansson
f5aa4f5866 Fix manual update for Command Line (#94433)
Manual update command line
2023-06-14 21:17:04 -04:00
Yuxin Wang
0083649e43 Add unit inference for Amps and VA in APCUPSD integration (#94431)
* Add unit inference for Amps and VA

* Rename `init_integration` to `async_init_integration` for better consistency with HA naming style
2023-06-14 21:17:03 -04:00
Sander
2505de35c9 Fix: Xiaomi Miio Fan, delay off countdown unit conversion (#94428) 2023-06-14 21:17:02 -04:00
Christopher Bailey
238eebb0b6 Bump unifiprotect to 4.10.3 (#94416)
* Bump unifiprotect to 4.10.3

* Reqs
2023-06-14 21:17:01 -04:00
Matthias Alphart
4cb30e69ac Update knx-frontend to 2023.6.9.195839 (#94404) 2023-06-14 21:16:19 -04:00
Joost Lekkerkerker
ac00977e57 Abort youtube configuration if user has no channel (#94402)
* Abort configuration if user has no channel

* Clean up

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-06-14 21:15:21 -04:00
jasonkuster
b2db849798 Fix ZHA binding api to actually return responses (#94388) 2023-06-14 21:15:20 -04:00
Raman Gupta
2c7a176580 Re-add event listeners after Z-Wave server disconnection (#94383)
* Re-add event listeners after Z-Wave server disconnection

* switch order

* Add tests
2023-06-14 21:15:19 -04:00
Matthias Alphart
4dbc408696 Update xknxproject to 3.1.1 (#94375) 2023-06-14 21:14:42 -04:00
Sebastian Muszynski
582fd11a70 Fix deprecated asyncio.wait use with coroutines (#94371) 2023-06-14 21:13:25 -04:00
Jan Bouwhuis
96cb5ff8b0 Fix dep noaa-coops for noaa_tides (#94370)
Bump noaa-coops to 0.1.9
2023-06-14 21:13:23 -04:00
Jafar Atili
6029e23ab7 fix: electrasmart - cast temperature to int in set_temperature (#94368)
fix: cast temperature to int
2023-06-14 21:13:22 -04:00
Jonathan Keljo
3434d74993 Upgrade sisyphus-control to 3.1.3 (#94310) 2023-06-14 21:13:21 -04:00
Nathan Spencer
e091793b6c Bump pylitterbot to 2023.4.2 (#94301) 2023-06-14 21:13:20 -04:00
Glenn Waters
9c8444da0e Bump elkm1-lib to 2.2.5 (#94296)
Co-authored-by: J. Nick Koston <nick@koston.org>
2023-06-14 21:13:19 -04:00
Tom Harris
427f0f4bee Fix issue with Insteon linked devices maintaining current state (#94286)
* Bump pyinsteon

* Update tests
2023-06-14 21:13:18 -04:00
FFT
95528f875e Change pyoppleio to pyoppleio-legacy (#88050)
* Change pyoppleio to pyoppleio-310 (#75268)

* [m] change opple component's dependency to a new working one
2023-06-14 21:13:17 -04:00
Paulus Schoutsen
a5f86bff45 2023.6.1 (#94288) 2023-06-08 14:57:37 -04:00
Paulus Schoutsen
d991970754 Bumped version to 2023.6.1 2023-06-08 13:39:33 -04:00
Tom Harris
d745b44180 Fix Insteon startup for users with X10 devices (#94277) 2023-06-08 13:39:10 -04:00
Tom Harris
602fcd6b1b Restructure Insteon start-up (#92818)
* Restructure startup

* Code review

* Further typing

* Fix circular import
2023-06-08 13:39:09 -04:00
Franck Nijhof
b39b0a960e Fix repair issue about no yaml for config entries (#94271) 2023-06-08 13:35:08 -04:00
Paulus Schoutsen
40bb796f03 Fix default value when logger used (#94269) 2023-06-08 13:28:54 -04:00
Christopher Bailey
2801ba6cad Bump unifiprotect to 4.10.2 (#94263) 2023-06-08 13:28:52 -04:00
Paul Bottein
5da0ef36ea Update frontend to 20230608.0 (#94256) 2023-06-08 13:28:51 -04:00
Joost Lekkerkerker
d861292900 Retrieve friends in an async manner in Lastfm (#94255) 2023-06-08 13:28:50 -04:00
Jc2k
a3fda43c64 Bump aiohomekit to 2.6.5 (fixes python 3.11 regression) (#94245) 2023-06-08 13:28:49 -04:00
Joost Lekkerkerker
8705a26a1a Catch exception when user has no lastfm friends (#94235) 2023-06-08 13:28:48 -04:00
jan iversen
2b1c45c28c Solve wrong return code from modbus. (#94234) 2023-06-08 13:28:47 -04:00
Jan Bouwhuis
0cf3825183 Fix imap crash on email without subject (#94230) 2023-06-08 13:28:46 -04:00
Kostas Chatzikokolakis
413e1c97d7 Bump pulsectl to 23.5.2 (#94227) 2023-06-08 13:28:45 -04:00
Joost Lekkerkerker
3b27a3aabf Bump python-opensky to 0.0.9 (#94224) 2023-06-08 13:28:44 -04:00
Joost Lekkerkerker
4509e13ceb Bump python-opensky (#93916) 2023-06-08 13:28:43 -04:00
Álvaro Fernández Rojas
33bf8c600b Update aioairzone-cloud to v0.1.8 (#94223) 2023-06-08 13:27:46 -04:00
Jan-Philipp Benecke
b508875f17 Set httpx log level to warning (#94217)
Set log level of httpx to warning
2023-06-08 13:27:45 -04:00
Allen Porter
ac963a2b6e Require pydantic 1.10.8 or higher (#94208)
* Requied pydantic 1.10.9 or higher

* Simplify constraint to 2.0

* Drop constraint by one patch release to 1.10.8 or higher

* Add package constraints to gen requirements script
2023-06-08 13:27:43 -04:00
James Connor
13029cf26f Fix ambiclimate for Python 3.11 (#94203)
Fix ambiclimate python 3.11 break
2023-06-08 13:27:42 -04:00
Paulus Schoutsen
f39a6b96ff Rename Local Media to My Media (#94201)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-06-08 13:27:41 -04:00
Mick Vleeshouwer
c6a17d6832 Bump pyoverkiz to 1.8.0 (#94176) 2023-06-08 13:27:40 -04:00
Joost Lekkerkerker
74c0552a12 Fix Abode unit of measurement (#94168)
Change unit of measurement to HA const
2023-06-08 13:27:38 -04:00
Justin Vanderhooft
f24b514c9a Bump melnor-bluetooth to fix a timezone issue (#94159) 2023-06-08 13:27:37 -04:00
Erik Montnemery
e1c47fdb61 Fix OTBR reset (#94157) 2023-06-08 13:27:36 -04:00
j4n-e4t
93baf24394 Add error handling to input_select integration (#93940) 2023-06-08 12:27:23 -04:00
Franck Nijhof
a4e236d0b9 2023.6.0 (#94158) 2023-06-07 15:39:47 +02:00
Franck Nijhof
421fa5b035 Bumped version to 2023.6.0 2023-06-07 13:49:03 +02:00
Erik Montnemery
3d3fecbd23 Disable google assistant local control of climate entities (#94153) 2023-06-07 13:48:20 +02:00
Erik Montnemery
468be632fd Add debug logs to cloud migration (#94151) 2023-06-07 13:48:17 +02:00
Bram Kragten
74ccdcda68 Update frontend to 20230607.0 (#94150) 2023-06-07 13:48:14 +02:00
Erik Montnemery
5cc61acfb2 Fix migration of Google Assistant cloud settings (#94148) 2023-06-07 13:48:11 +02:00
Christopher Bailey
02d55a8e49 Bump unifiprotect to 4.10.1 (#94141) 2023-06-07 13:48:06 +02:00
Paulus Schoutsen
f4e3ef6b51 Bumped version to 2023.6.0b6 2023-06-06 22:00:28 -04:00
Paulus Schoutsen
7740539df0 Bump waqiasync to 1.1.0 (#94136) 2023-06-06 22:00:20 -04:00
Christopher Bailey
b077bf9b86 Fix multiple smart detects firing at once for UniFi Protect (#94133)
* Fix multiple smart detects firing at once

* Tweak

* Clean up logging. Linting

* Linting
2023-06-06 22:00:19 -04:00
Joakim Plate
23f2898836 Correct zha device classes for voc and pm25 (#94130)
Correct zha device classes
2023-06-06 22:00:18 -04:00
Shay Levy
e6638ca356 Remove goalfeed integration (#94129) 2023-06-06 21:59:32 -04:00
Jean-François Roy
93d52d8835 Bump aiobafi6 to 0.8.2 (#94125) 2023-06-06 21:58:09 -04:00
puddly
26e08abb9a Revert "Increase Zigbee command retries (#93877)" (#94123) 2023-06-06 21:58:08 -04:00
J. Nick Koston
6a573b507e Remove mark_read service from persistent_notification (#94122)
* Remove mark_read from persistent_notification

Nothing on the frontend uses this, and the service is not documented

There is not much point in keeping this as the notifications
are no longer stored in the state machine

* adjust

* adjust
2023-06-06 21:58:07 -04:00
Bram Kragten
2b39550e55 Update frontend to 20230606.0 (#94119) 2023-06-06 21:58:06 -04:00
J. Nick Koston
0e50baf007 Verify persistant notifications can be dismissed by the id they are created with (#94112) 2023-06-06 21:58:05 -04:00
Luke
286de1f051 Bump python-roborock to 23.4 (#94111)
* bump to 23.0

* bump to 23.4
2023-06-06 21:58:04 -04:00
Luke
3e23996247 Bump Roborock to 0.21.0 (#94035)
bump to 21.0
2023-06-06 21:58:03 -04:00
Álvaro Fernández Rojas
7a658117bb Update aioairzone to v0.6.3 and fix issue with latest firmware update (#94100) 2023-06-06 21:56:36 -04:00
Luke
49388eab3a Add diagnostics to Roborock (#94099)
* Add diagnostics

* Update homeassistant/components/roborock/models.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* adds snapshot

---------

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2023-06-06 21:56:35 -04:00
Justin Vanderhooft
e6fcc6b73c fix: Bump melnor-bluetooth to fix deadlock (#94098) 2023-06-06 21:56:33 -04:00
Robert Svensson
e00012289d Bump aiounifi to v48 - Fix fail to initialise due to board_rev not exist (#94093) 2023-06-06 21:56:32 -04:00
Luke
f373f1abd5 Add missing translation keys for Roborock mop intensity (#94088) 2023-06-06 21:56:31 -04:00
puddly
2c43672a8a Include port info in the ZHA websocket settings response (#93934) 2023-06-06 21:56:30 -04:00
Paulus Schoutsen
7a6327d7e2 Bumped version to 2023.6.0b5 2023-06-05 16:13:07 -04:00
G Johansson
ee8f63b9c9 Fix reload service in Command Line (#94085)
Fix multi platform reload service in command line
2023-06-05 16:12:59 -04:00
Bram Kragten
28e0f5e104 Update frontend to 20230605.0 (#94083)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-06-05 16:12:58 -04:00
Michael Hansen
eb036af410 Bump intents to 2023.6.5 (#94077) 2023-06-05 16:12:57 -04:00
Luke
4bb6fec1d6 Don't add Roborock switches if it is not supported (#94069)
* don't add switches if it is not supported

* don't create entity unless if it is valid

* Raise on other exceptions

* rework valid_enties
2023-06-05 16:12:56 -04:00
J. Nick Koston
dbd5511e5e Bump zeroconf to 0.64.0 (#94052) 2023-06-05 16:12:55 -04:00
Raman Gupta
580065e946 Fix zwave_js.update entity restore logic (#94043) 2023-06-05 16:12:54 -04:00
Pascal Reeb
4a31cb0ad8 Update pynuki to 1.6.2 (#94041)
chore(component/nuki): update pynuki to 1.6.2
2023-06-05 16:12:53 -04:00
G Johansson
5a63079c80 Remove update_before_add from binary_sensor in Command Line (#94040)
Remove update_before_add
2023-06-05 16:12:52 -04:00
tronikos
902bd521d2 Android TV Remote: Abort zeroconf if mac address is missing (#94026)
Abort zeroconf if mac address is missing
2023-06-05 16:12:51 -04:00
Ernst Klamer
aff4d537a7 Bump xiaomi-ble to 0.17.2 (#94011)
Bump xiaomi-ble

Co-authored-by: J. Nick Koston <nick@koston.org>
2023-06-05 16:12:49 -04:00
Joost Lekkerkerker
4f00cc9faa Show the sensor state using the coordinatordata instead of initial data (#94008)
* Show the sensor state using the coordinatordata instead of initial data

* Add test

* Remove part
2023-06-05 16:12:48 -04:00
Joost Lekkerkerker
2a99fea1de Add video id to youtube sensor state attributes (#93668)
* Add video id to state attributes

* Make extra state attributes not optional

* Revert "Make extra state attributes not optional"

This reverts commit d2f9e936c809dd50a5e4bbdaa181c9c9ddd3d217.
2023-06-05 16:12:48 -04:00
tronikos
9aeba6221b Fix error in tibber while fetching latest statistics (#93998) 2023-06-05 15:55:59 -04:00
Paulus Schoutsen
bb2a89f065 Bumped version to 2023.6.0b4 2023-06-02 23:35:41 -04:00
Robert Hillis
f92298c6fc Catch Google Sheets api error (#93979) 2023-06-02 23:35:36 -04:00
G Johansson
6ff55a6505 Add scan interval to Command Line (#93752)
* Add scan interval

* Handle previous not complete

* Fix faulty text

* Add tests

* lingering

* Cool down

* Fix tests
2023-06-02 23:35:35 -04:00
Paulus Schoutsen
32f7f39eca Bumped version to 2023.6.0b3 2023-06-02 08:46:29 -04:00
Raman Gupta
177cd0f697 Improve logic for zwave_js.lock.is_locked attr (#93947) 2023-06-02 08:45:56 -04:00
Raman Gupta
3d4ba15a95 Make Z-Wave device IBT4ZWAVE discoverable as a cover (#93946)
* Make Z-Wave device IBT4ZWAVE discoverable as a cover

* Test device class
2023-06-02 08:45:55 -04:00
J. Nick Koston
9dd3e6cab8 Bump aiohomekit to 2.6.4 (#93943)
changelog: https://github.com/Jc2k/aiohomekit/compare/2.6.3...2.6.4

mostly additional logging to help track down #93891
2023-06-02 08:45:54 -04:00
J. Nick Koston
cc02d1dfc4 Fix august aiohttp session being closed out from under it (#93942)
* Fix august aiohttp session being closed out from under it

fixes #93941

* Fix august aiohttp session being closed out from under it

fixes #93941

* Fix august aiohttp session being closed out from under it

fixes #93941
2023-06-02 08:45:53 -04:00
automaton82
3d2ad2fd85 Update netdata to 1.1.0, set longer timeout (#93937) 2023-06-02 08:45:52 -04:00
Chris Talkington
d9149407d8 Update pyipp to 0.13.0 (#93886) 2023-06-02 08:45:51 -04:00
Diogo Gomes
964af88c21 Make Riemann sum sensors restore last valid state (#93674)
* keep last valid state

* keep last valid state

* typo

* increase coverage

* better error handling

* debug messages

* increase coverage

* remove random log

* don't expose last_valid_state as an attribute
2023-06-02 08:45:50 -04:00
Tudor Sandu
e58ea00ce6 Fix states not being translated in voice assistants (#93572)
Fix states not being translated
2023-06-02 08:45:49 -04:00
181 changed files with 4534 additions and 835 deletions

View File

@@ -420,7 +420,6 @@ omit =
homeassistant/components/gitlab_ci/sensor.py
homeassistant/components/gitter/sensor.py
homeassistant/components/glances/sensor.py
homeassistant/components/goalfeed/*
homeassistant/components/goodwe/__init__.py
homeassistant/components/goodwe/button.py
homeassistant/components/goodwe/coordinator.py

View File

@@ -1,11 +1,11 @@
image: homeassistant/{arch}-homeassistant
shadow_repository: ghcr.io/home-assistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.05.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.05.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.05.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.05.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.05.0
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.06.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.06.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.06.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.06.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.06.0
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io

View File

@@ -391,6 +391,7 @@ def async_enable_logging(
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
sys.excepthook = lambda *args: logging.getLogger(None).exception(
"Uncaught exception", exc_info=args # type: ignore[arg-type]

View File

@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import LIGHT_LUX
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -71,7 +72,7 @@ class AbodeSensor(AbodeDevice, SensorEntity):
elif description.key == CONST.HUMI_STATUS_KEY:
self._attr_native_unit_of_measurement = device.humidity_unit
elif description.key == CONST.LUX_STATUS_KEY:
self._attr_native_unit_of_measurement = device.lux_unit
self._attr_native_unit_of_measurement = LIGHT_LUX
@property
def native_value(self) -> float | None:

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.6.1"]
"requirements": ["aioairzone==0.6.3"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_polling",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.1.7"]
"requirements": ["aioairzone-cloud==0.1.8"]
}

View File

@@ -98,7 +98,7 @@ async def async_setup_entry(
tasks = []
for heater in data_connection.get_devices():
tasks.append(heater.update_device_info())
tasks.append(asyncio.create_task(heater.update_device_info()))
await asyncio.wait(tasks)
devs = []

View File

@@ -135,7 +135,8 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.host = discovery_info.host
self.name = discovery_info.name.removesuffix("._androidtvremote2._tcp.local.")
self.mac = discovery_info.properties.get("bt")
assert self.mac
if not self.mac:
return self.async_abort(reason="cannot_connect")
await self.async_set_unique_id(format_mac(self.mac))
self._abort_if_unique_id_configured(
updates={CONF_HOST: self.host, CONF_NAME: self.name}

View File

@@ -430,7 +430,9 @@ INFERRED_UNITS = {
" Percent": PERCENTAGE,
" Volts": UnitOfElectricPotential.VOLT,
" Ampere": UnitOfElectricCurrent.AMPERE,
" Amps": UnitOfElectricCurrent.AMPERE,
" Volt-Ampere": UnitOfApparentPower.VOLT_AMPERE,
" VA": UnitOfApparentPower.VOLT_AMPERE,
" Watts": UnitOfPower.WATT,
" Hz": UnitOfFrequency.HERTZ,
" C": UnitOfTemperature.CELSIUS,

View File

@@ -23,7 +23,7 @@ from homeassistant.exceptions import (
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import device_registry as dr, discovery_flow
from homeassistant.helpers import aiohttp_client, device_registry as dr, discovery_flow
from .activity import ActivityStream
from .const import CONF_BRAND, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS
@@ -44,8 +44,11 @@ YALEXS_BLE_DOMAIN = "yalexs_ble"
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up August from a config entry."""
august_gateway = AugustGateway(hass)
# Create an aiohttp session instead of using the default one since the
# default one is likely to trigger august's WAF if another integration
# is also using Cloudflare
session = aiohttp_client.async_create_clientsession(hass)
august_gateway = AugustGateway(hass, session)
try:
await august_gateway.async_setup(entry.data)

View File

@@ -4,13 +4,16 @@ from dataclasses import dataclass
import logging
from typing import Any
import aiohttp
import voluptuous as vol
from yalexs.authenticator import ValidationResult
from yalexs.const import BRANDS, DEFAULT_BRAND
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client
from .const import (
CONF_ACCESS_TOKEN_CACHE_FILE,
@@ -80,6 +83,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Store an AugustGateway()."""
self._august_gateway: AugustGateway | None = None
self._aiohttp_session: aiohttp.ClientSession | None = None
self._user_auth_details: dict[str, Any] = {}
self._needs_reset = True
self._mode = None
@@ -87,7 +91,6 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
self._august_gateway = AugustGateway(self.hass)
return await self.async_step_user_validate()
async def async_step_user_validate(self, user_input=None):
@@ -151,12 +154,30 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
},
)
@callback
def _async_get_gateway(self) -> AugustGateway:
"""Set up the gateway."""
if self._august_gateway is not None:
return self._august_gateway
# Create an aiohttp session instead of using the default one since the
# default one is likely to trigger august's WAF if another integration
# is also using Cloudflare
self._aiohttp_session = aiohttp_client.async_create_clientsession(self.hass)
self._august_gateway = AugustGateway(self.hass, self._aiohttp_session)
return self._august_gateway
@callback
def _async_shutdown_gateway(self) -> None:
"""Shutdown the gateway."""
if self._aiohttp_session is not None:
self._aiohttp_session.detach()
self._august_gateway = None
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle configuration by re-auth."""
self._user_auth_details = dict(entry_data)
self._mode = "reauth"
self._needs_reset = True
self._august_gateway = AugustGateway(self.hass)
return await self.async_step_reauth_validate()
async def async_step_reauth_validate(self, user_input=None):
@@ -206,7 +227,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _async_auth_or_validate(self) -> ValidateResult:
"""Authenticate or validate."""
user_auth_details = self._user_auth_details
gateway = self._august_gateway
gateway = self._async_get_gateway()
assert gateway is not None
await self._async_reset_access_token_cache_if_needed(
gateway,
@@ -239,6 +260,8 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _async_update_or_create_entry(self, info: dict[str, Any]) -> FlowResult:
"""Update existing entry or create a new one."""
self._async_shutdown_gateway()
existing_entry = await self.async_set_unique_id(
self._user_auth_details[CONF_USERNAME]
)

View File

@@ -7,7 +7,7 @@ import logging
import os
from typing import Any
from aiohttp import ClientError, ClientResponseError
from aiohttp import ClientError, ClientResponseError, ClientSession
from yalexs.api_async import ApiAsync
from yalexs.authenticator_async import AuthenticationState, AuthenticatorAsync
from yalexs.authenticator_common import Authentication
@@ -16,7 +16,6 @@ from yalexs.exceptions import AugustApiAIOHTTPError
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client
from .const import (
CONF_ACCESS_TOKEN_CACHE_FILE,
@@ -35,12 +34,9 @@ _LOGGER = logging.getLogger(__name__)
class AugustGateway:
"""Handle the connection to August."""
def __init__(self, hass: HomeAssistant) -> None:
def __init__(self, hass: HomeAssistant, aiohttp_session: ClientSession) -> None:
"""Init the connection."""
# Create an aiohttp session instead of using the default one since the
# default one is likely to trigger august's WAF if another integration
# is also using Cloudflare
self._aiohttp_session = aiohttp_client.async_create_clientsession(hass)
self._aiohttp_session = aiohttp_session
self._token_refresh_lock = asyncio.Lock()
self._access_token_cache_file: str | None = None
self._hass: HomeAssistant = hass

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==1.5.1", "yalexs-ble==2.1.17"]
"requirements": ["yalexs==1.5.1", "yalexs-ble==2.1.18"]
}

View File

@@ -5,7 +5,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/baf",
"iot_class": "local_push",
"requirements": ["aiobafi6==0.8.0"],
"requirements": ["aiobafi6==0.8.2"],
"zeroconf": [
{
"type": "_api._tcp.local.",

View File

@@ -221,6 +221,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
async def on_hass_started(hass: HomeAssistant) -> None:
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
_LOGGER.info(
"Start migration of Alexa settings from v%s to v%s",
self._prefs.alexa_settings_version,
ALEXA_SETTINGS_VERSION,
)
if self._prefs.alexa_settings_version < 2 or (
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
self._prefs.alexa_settings_version < 3
@@ -233,6 +238,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
):
self._migrate_alexa_entity_settings_v1()
_LOGGER.info(
"Finished migration of Alexa settings from v%s to v%s",
self._prefs.alexa_settings_version,
ALEXA_SETTINGS_VERSION,
)
await self._prefs.async_update(
alexa_settings_version=ALEXA_SETTINGS_VERSION
)

View File

@@ -108,7 +108,12 @@ def _supported_legacy(hass: HomeAssistant, entity_id: str) -> bool:
if domain in SUPPORTED_DOMAINS:
return True
device_class = get_device_class(hass, entity_id)
try:
device_class = get_device_class(hass, entity_id)
except HomeAssistantError:
# The entity no longer exists
return False
if (
domain == "binary_sensor"
and device_class in SUPPORTED_BINARY_SENSOR_DEVICE_CLASSES
@@ -208,6 +213,11 @@ class CloudGoogleConfig(AbstractConfig):
async def on_hass_started(hass: HomeAssistant) -> None:
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
_LOGGER.info(
"Start migration of Google Assistant settings from v%s to v%s",
self._prefs.google_settings_version,
GOOGLE_SETTINGS_VERSION,
)
if self._prefs.google_settings_version < 2 or (
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
self._prefs.google_settings_version < 3
@@ -220,6 +230,11 @@ class CloudGoogleConfig(AbstractConfig):
):
self._migrate_google_entity_settings_v1()
_LOGGER.info(
"Finished migration of Google Assistant settings from v%s to v%s",
self._prefs.google_settings_version,
GOOGLE_SETTINGS_VERSION,
)
await self._prefs.async_update(
google_settings_version=GOOGLE_SETTINGS_VERSION
)

View File

@@ -11,16 +11,24 @@ import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as BINARY_SENSOR_DOMAIN,
SCAN_INTERVAL as BINARY_SENSOR_DEFAULT_SCAN_INTERVAL,
)
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
SCAN_INTERVAL as COVER_DEFAULT_SCAN_INTERVAL,
)
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as SENSOR_DOMAIN,
SCAN_INTERVAL as SENSOR_DEFAULT_SCAN_INTERVAL,
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SCAN_INTERVAL as SWITCH_DEFAULT_SCAN_INTERVAL,
)
from homeassistant.const import (
CONF_COMMAND,
CONF_COMMAND_CLOSE,
@@ -34,15 +42,19 @@ from homeassistant.const import (
CONF_NAME,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
SERVICE_RELOAD,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
@@ -74,6 +86,9 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(
CONF_SCAN_INTERVAL, default=BINARY_SENSOR_DEFAULT_SCAN_INTERVAL
): vol.All(cv.time_period, cv.positive_timedelta),
}
)
COVER_SCHEMA = vol.Schema(
@@ -86,6 +101,9 @@ COVER_SCHEMA = vol.Schema(
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=COVER_DEFAULT_SCAN_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
NOTIFY_SCHEMA = vol.Schema(
@@ -106,6 +124,9 @@ SENSOR_SCHEMA = vol.Schema(
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
vol.Optional(CONF_SCAN_INTERVAL, default=SENSOR_DEFAULT_SCAN_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
SWITCH_SCHEMA = vol.Schema(
@@ -118,6 +139,9 @@ SWITCH_SCHEMA = vol.Schema(
vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SWITCH_DEFAULT_SCAN_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
COMBINED_SCHEMA = vol.Schema(
@@ -142,22 +166,49 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Command Line from yaml config."""
command_line_config: list[dict[str, dict[str, Any]]] = config.get(DOMAIN, [])
async def _reload_config(call: Event | ServiceCall) -> None:
"""Reload Command Line."""
reload_config = await async_integration_yaml_config(hass, "command_line")
reset_platforms = async_get_platforms(hass, "command_line")
for reset_platform in reset_platforms:
_LOGGER.debug("Reload resetting platform: %s", reset_platform.domain)
await reset_platform.async_reset()
if not reload_config:
return
await async_load_platforms(hass, reload_config.get(DOMAIN, []), reload_config)
async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config)
await async_load_platforms(hass, config.get(DOMAIN, []), config)
return True
async def async_load_platforms(
hass: HomeAssistant,
command_line_config: list[dict[str, dict[str, Any]]],
config: ConfigType,
) -> None:
"""Load platforms from yaml."""
if not command_line_config:
return True
return
_LOGGER.debug("Full config loaded: %s", command_line_config)
load_coroutines: list[Coroutine[Any, Any, None]] = []
platforms: list[Platform] = []
reload_configs: list[tuple] = []
for platform_config in command_line_config:
for platform, _config in platform_config.items():
platforms.append(PLATFORM_MAPPING[platform])
if (mapped_platform := PLATFORM_MAPPING[platform]) not in platforms:
platforms.append(mapped_platform)
_LOGGER.debug(
"Loading config %s for platform %s",
platform_config,
PLATFORM_MAPPING[platform],
)
reload_configs.append((PLATFORM_MAPPING[platform], _config))
load_coroutines.append(
discovery.async_load_platform(
hass,
@@ -168,10 +219,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
)
await async_setup_reload_service(hass, DOMAIN, platforms)
if load_coroutines:
_LOGGER.debug("Loading platforms: %s", platforms)
await asyncio.gather(*load_coroutines)
return True

View File

@@ -1,6 +1,7 @@
"""Support for custom shell commands to retrieve values."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import voluptuous as vol
@@ -18,17 +19,20 @@ from homeassistant.const import (
CONF_NAME,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .sensor import CommandSensorData
DEFAULT_NAME = "Binary Command Sensor"
@@ -84,6 +88,9 @@ async def async_setup_platform(
value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
command_timeout: int = binary_sensor_config[CONF_COMMAND_TIMEOUT]
unique_id: str | None = binary_sensor_config.get(CONF_UNIQUE_ID)
scan_interval: timedelta = binary_sensor_config.get(
CONF_SCAN_INTERVAL, SCAN_INTERVAL
)
if value_template is not None:
value_template.hass = hass
data = CommandSensorData(hass, command, command_timeout)
@@ -98,15 +105,17 @@ async def async_setup_platform(
payload_off,
value_template,
unique_id,
scan_interval,
)
],
True,
)
class CommandBinarySensor(BinarySensorEntity):
"""Representation of a command line binary sensor."""
_attr_should_poll = False
def __init__(
self,
data: CommandSensorData,
@@ -116,6 +125,7 @@ class CommandBinarySensor(BinarySensorEntity):
payload_off: str,
value_template: Template | None,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the Command line binary sensor."""
self.data = data
@@ -126,8 +136,39 @@ class CommandBinarySensor(BinarySensorEntity):
self._payload_off = payload_off
self._value_template = value_template
self._attr_unique_id = unique_id
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_update(self) -> None:
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
await self._update_entity_state(None)
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Binary Sensor - {self.name}",
cancel_on_shutdown=True,
),
)
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Binary Sensor %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Get the latest data and updates the state."""
await self.hass.async_add_executor_job(self.data.update)
value = self.data.value
@@ -141,3 +182,12 @@ class CommandBinarySensor(BinarySensorEntity):
self._attr_is_on = True
elif value == self._payload_off:
self._attr_is_on = False
self.async_write_ha_state()
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self._update_entity_state(dt_util.now())

View File

@@ -1,7 +1,11 @@
"""Allows to configure custom shell commands to turn a value for a sensor."""
import logging
from homeassistant.const import Platform
LOGGER = logging.getLogger(__package__)
CONF_COMMAND_TIMEOUT = "command_timeout"
DEFAULT_TIMEOUT = 15
DOMAIN = "command_line"

View File

@@ -1,7 +1,8 @@
"""Support for command line covers."""
from __future__ import annotations
import logging
import asyncio
from datetime import timedelta
from typing import TYPE_CHECKING, Any
import voluptuous as vol
@@ -19,21 +20,23 @@ from homeassistant.const import (
CONF_COVERS,
CONF_FRIENDLY_NAME,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify
from homeassistant.util import dt as dt_util, slugify
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import call_shell_with_timeout, check_output_or_log
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=15)
COVER_SCHEMA = vol.Schema(
{
@@ -97,11 +100,12 @@ async def async_setup_platform(
value_template,
device_config[CONF_COMMAND_TIMEOUT],
device_config.get(CONF_UNIQUE_ID),
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
)
)
if not covers:
_LOGGER.error("No covers added")
LOGGER.error("No covers added")
return
async_add_entities(covers)
@@ -110,6 +114,8 @@ async def async_setup_platform(
class CommandCover(CoverEntity):
"""Representation a command line cover."""
_attr_should_poll = False
def __init__(
self,
name: str,
@@ -120,6 +126,7 @@ class CommandCover(CoverEntity):
value_template: Template | None,
timeout: int,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the cover."""
self._attr_name = name
@@ -131,17 +138,32 @@ class CommandCover(CoverEntity):
self._value_template = value_template
self._timeout = timeout
self._attr_unique_id = unique_id
self._attr_should_poll = bool(command_state)
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
if self._command_state:
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Cover - {self.name}",
cancel_on_shutdown=True,
),
)
def _move_cover(self, command: str) -> bool:
"""Execute the actual commands."""
_LOGGER.info("Running command: %s", command)
LOGGER.info("Running command: %s", command)
returncode = call_shell_with_timeout(command, self._timeout)
success = returncode == 0
if not success:
_LOGGER.error(
LOGGER.error(
"Command failed (with return code %s): %s", returncode, command
)
@@ -165,12 +187,27 @@ class CommandCover(CoverEntity):
def _query_state(self) -> str | None:
"""Query for the state."""
if self._command_state:
_LOGGER.info("Running state value command: %s", self._command_state)
LOGGER.info("Running state value command: %s", self._command_state)
return check_output_or_log(self._command_state, self._timeout)
if TYPE_CHECKING:
return None
async def async_update(self) -> None:
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Cover %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Update device state."""
if self._command_state:
payload = str(await self.hass.async_add_executor_job(self._query_state))
@@ -181,15 +218,26 @@ class CommandCover(CoverEntity):
self._state = None
if payload:
self._state = int(payload)
await self.async_update_ha_state(True)
def open_cover(self, **kwargs: Any) -> None:
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self._update_entity_state(dt_util.now())
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
self._move_cover(self._command_open)
await self.hass.async_add_executor_job(self._move_cover, self._command_open)
await self._update_entity_state(None)
def close_cover(self, **kwargs: Any) -> None:
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
self._move_cover(self._command_close)
await self.hass.async_add_executor_job(self._move_cover, self._command_close)
await self._update_entity_state(None)
def stop_cover(self, **kwargs: Any) -> None:
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
self._move_cover(self._command_stop)
await self.hass.async_add_executor_job(self._move_cover, self._command_stop)
await self._update_entity_state(None)

View File

@@ -1,10 +1,10 @@
"""Allows to configure custom shell commands to turn a value for a sensor."""
from __future__ import annotations
import asyncio
from collections.abc import Mapping
from datetime import timedelta
import json
import logging
import voluptuous as vol
@@ -20,6 +20,7 @@ from homeassistant.const import (
CONF_COMMAND,
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
@@ -28,15 +29,15 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import check_output_or_log
_LOGGER = logging.getLogger(__name__)
CONF_JSON_ATTRIBUTES = "json_attributes"
DEFAULT_NAME = "Command Sensor"
@@ -88,6 +89,7 @@ async def async_setup_platform(
if value_template is not None:
value_template.hass = hass
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
data = CommandSensorData(hass, command, command_timeout)
async_add_entities(
@@ -99,15 +101,17 @@ async def async_setup_platform(
value_template,
json_attributes,
unique_id,
scan_interval,
)
],
True,
]
)
class CommandSensor(SensorEntity):
"""Representation of a sensor that is using shell commands."""
_attr_should_poll = False
def __init__(
self,
data: CommandSensorData,
@@ -116,6 +120,7 @@ class CommandSensor(SensorEntity):
value_template: Template | None,
json_attributes: list[str] | None,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the sensor."""
self._attr_name = name
@@ -126,8 +131,39 @@ class CommandSensor(SensorEntity):
self._value_template = value_template
self._attr_native_unit_of_measurement = unit_of_measurement
self._attr_unique_id = unique_id
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_update(self) -> None:
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
await self._update_entity_state(None)
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Sensor - {self.name}",
cancel_on_shutdown=True,
),
)
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Sensor %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Get the latest data and updates the state."""
await self.hass.async_add_executor_job(self.data.update)
value = self.data.value
@@ -144,11 +180,11 @@ class CommandSensor(SensorEntity):
if k in json_dict
}
else:
_LOGGER.warning("JSON result was not a dictionary")
LOGGER.warning("JSON result was not a dictionary")
except ValueError:
_LOGGER.warning("Unable to parse output as JSON: %s", value)
LOGGER.warning("Unable to parse output as JSON: %s", value)
else:
_LOGGER.warning("Empty reply found when expecting JSON data")
LOGGER.warning("Empty reply found when expecting JSON data")
if self._value_template is None:
self._attr_native_value = None
return
@@ -163,6 +199,15 @@ class CommandSensor(SensorEntity):
else:
self._attr_native_value = value
self.async_write_ha_state()
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self._update_entity_state(dt_util.now())
class CommandSensorData:
"""The class for handling the data retrieval."""
@@ -191,7 +236,7 @@ class CommandSensorData:
args_to_render = {"arguments": args}
rendered_args = args_compiled.render(args_to_render)
except TemplateError as ex:
_LOGGER.exception("Error rendering command template: %s", ex)
LOGGER.exception("Error rendering command template: %s", ex)
return
else:
rendered_args = None
@@ -203,5 +248,5 @@ class CommandSensorData:
# Template used. Construct the string used in the shell
command = f"{prog} {rendered_args}"
_LOGGER.debug("Running command: %s", command)
LOGGER.debug("Running command: %s", command)
self.value = check_output_or_log(command, self.timeout)

View File

@@ -1,7 +1,8 @@
"""Support for custom shell commands to turn a switch on/off."""
from __future__ import annotations
import logging
import asyncio
from datetime import timedelta
from typing import TYPE_CHECKING, Any
import voluptuous as vol
@@ -20,6 +21,7 @@ from homeassistant.const import (
CONF_ICON,
CONF_ICON_TEMPLATE,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_SWITCHES,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
@@ -27,16 +29,17 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.template_entity import ManualTriggerEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify
from homeassistant.util import dt as dt_util, slugify
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import call_shell_with_timeout, check_output_or_log
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=30)
SWITCH_SCHEMA = vol.Schema(
{
@@ -112,11 +115,12 @@ async def async_setup_platform(
device_config.get(CONF_COMMAND_STATE),
value_template,
device_config[CONF_COMMAND_TIMEOUT],
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
)
)
if not switches:
_LOGGER.error("No switches added")
LOGGER.error("No switches added")
return
async_add_entities(switches)
@@ -125,6 +129,8 @@ async def async_setup_platform(
class CommandSwitch(ManualTriggerEntity, SwitchEntity):
"""Representation a switch that can be toggled using shell commands."""
_attr_should_poll = False
def __init__(
self,
config: ConfigType,
@@ -134,6 +140,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
command_state: str | None,
value_template: Template | None,
timeout: int,
scan_interval: timedelta,
) -> None:
"""Initialize the switch."""
super().__init__(self.hass, config)
@@ -144,11 +151,26 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
self._command_state = command_state
self._value_template = value_template
self._timeout = timeout
self._attr_should_poll = bool(command_state)
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
if self._command_state:
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Cover - {self.name}",
cancel_on_shutdown=True,
),
)
async def _switch(self, command: str) -> bool:
"""Execute the actual commands."""
_LOGGER.info("Running command: %s", command)
LOGGER.info("Running command: %s", command)
success = (
await self.hass.async_add_executor_job(
@@ -158,18 +180,18 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
)
if not success:
_LOGGER.error("Command failed: %s", command)
LOGGER.error("Command failed: %s", command)
return success
def _query_state_value(self, command: str) -> str | None:
"""Execute state command for return value."""
_LOGGER.info("Running state value command: %s", command)
LOGGER.info("Running state value command: %s", command)
return check_output_or_log(command, self._timeout)
def _query_state_code(self, command: str) -> bool:
"""Execute state command for return code."""
_LOGGER.info("Running state code command: %s", command)
LOGGER.info("Running state code command: %s", command)
return (
call_shell_with_timeout(command, self._timeout, log_return_code=False) == 0
)
@@ -188,7 +210,22 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
if TYPE_CHECKING:
return None
async def async_update(self) -> None:
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Switch %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Update device state."""
if self._command_state:
payload = str(await self.hass.async_add_executor_job(self._query_state))
@@ -201,15 +238,25 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
if payload or value:
self._attr_is_on = (value or payload).lower() == "true"
self._process_manual_data(payload)
await self.async_update_ha_state(True)
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self._update_entity_state(dt_util.now())
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
if await self._switch(self._command_on) and not self._command_state:
self._attr_is_on = True
self.async_schedule_update_ha_state()
await self._update_entity_state(None)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
if await self._switch(self._command_off) and not self._command_state:
self._attr_is_on = False
self.async_schedule_update_ha_state()
await self._update_entity_state(None)

View File

@@ -278,13 +278,13 @@ class DefaultAgent(AbstractConversationAgent):
all_states = matched + unmatched
domains = {state.domain for state in all_states}
translations = await translation.async_get_translations(
self.hass, language, "state", domains
self.hass, language, "entity_component", domains
)
# Use translated state names
for state in all_states:
device_class = state.attributes.get("device_class", "_")
key = f"component.{state.domain}.state.{device_class}.{state.state}"
key = f"component.{state.domain}.entity_component.{device_class}.state.{state.state}"
state.state = translations.get(key, state.state)
# Get first matched or unmatched state.

View File

@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["hassil==1.0.6", "home-assistant-intents==2023.5.30"]
"requirements": ["hassil==1.0.6", "home-assistant-intents==2023.6.5"]
}

View File

@@ -7,6 +7,6 @@
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"quality_scale": "platinum",
"requirements": ["pydaikin==2.9.1"],
"requirements": ["pydaikin==2.9.0"],
"zeroconf": ["_dkapi._tcp.local."]
}

View File

@@ -42,7 +42,7 @@ async def async_setup_entry(
[
DaikinZoneSwitch(daikin_api, zone_id)
for zone_id, zone in enumerate(zones)
if zone[0] != ("-", "0")
if zone != ("-", "0")
]
)
if daikin_api.device.support_advanced_modes:

View File

@@ -250,7 +250,7 @@ class ElectraClimateEntity(ClimateEntity):
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
raise ValueError("No target temperature provided")
self._electra_ac_device.set_temperature(temperature)
self._electra_ac_device.set_temperature(int(temperature))
await self._async_operate_electra_ac()
def _update_device_attrs(self) -> None:

View File

@@ -15,5 +15,5 @@
"documentation": "https://www.home-assistant.io/integrations/elkm1",
"iot_class": "local_push",
"loggers": ["elkm1_lib"],
"requirements": ["elkm1-lib==2.2.2"]
"requirements": ["elkm1-lib==2.2.5"]
}

View File

@@ -192,7 +192,7 @@ class Flexit(ClimateEntity):
result = float(
await self._async_read_int16_from_register(register_type, register)
)
if result == -1:
if not result:
return -1
return result / 10.0
@@ -200,6 +200,6 @@ class Flexit(ClimateEntity):
result = await self._hub.async_pymodbus_call(
self._slave, register, value, CALL_TYPE_WRITE_REGISTER
)
if result == -1:
if not result:
return False
return True

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230601.1"]
"requirements": ["home-assistant-frontend==20230608.0"]
}

View File

@@ -1,65 +0,0 @@
"""Component for the Goalfeed service."""
import json
import pysher
import requests
import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
# Version downgraded due to regression in library
# For details: https://github.com/nlsdfnbch/Pysher/issues/38
DOMAIN = "goalfeed"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
GOALFEED_HOST = "feed.goalfeed.ca"
GOALFEED_AUTH_ENDPOINT = "https://goalfeed.ca/feed/auth"
GOALFEED_APP_ID = "bfd4ed98c1ff22c04074"
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Goalfeed component."""
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
def goal_handler(data):
"""Handle goal events."""
goal = json.loads(json.loads(data))
hass.bus.fire("goal", event_data=goal)
def connect_handler(data):
"""Handle connection."""
post_data = {
"username": username,
"password": password,
"connection_info": data,
}
resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data, timeout=30).json()
channel = pusher.subscribe("private-goals", resp["auth"])
channel.bind("goal", goal_handler)
pusher = pysher.Pusher(
GOALFEED_APP_ID, secure=False, port=8080, custom_host=GOALFEED_HOST
)
pusher.connection.bind("pusher:connection_established", connect_handler)
pusher.connect()
return True

View File

@@ -1,9 +0,0 @@
{
"domain": "goalfeed",
"name": "Goalfeed",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/goalfeed",
"iot_class": "cloud_push",
"loggers": ["pysher"],
"requirements": ["pysher==1.0.7"]
}

View File

@@ -186,7 +186,7 @@ STORE_GOOGLE_LOCAL_WEBHOOK_ID = "local_webhook_id"
SOURCE_CLOUD = "cloud"
SOURCE_LOCAL = "local"
NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK}
NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK, TYPE_THERMOSTAT}
FAN_SPEEDS = {
"5/5": ["High", "Max", "Fast", "5"],

View File

@@ -7,13 +7,18 @@ import aiohttp
from google.auth.exceptions import RefreshError
from google.oauth2.credentials import Credentials
from gspread import Client
from gspread.exceptions import APIError
from gspread.utils import ValueInputOption
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
@@ -93,6 +98,9 @@ async def async_setup_service(hass: HomeAssistant) -> None:
except RefreshError as ex:
entry.async_start_reauth(hass)
raise ex
except APIError as ex:
raise HomeAssistantError("Failed to write data") from ex
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
row_data = {"created": str(datetime.now())} | call.data[DATA]
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])

View File

@@ -96,19 +96,29 @@ class MultiprotocolAddonManager(AddonManager):
) -> None:
"""Register a multipan platform."""
self._platforms[integration_domain] = platform
if self._channel is not None or not await platform.async_using_multipan(hass):
channel = await platform.async_get_channel(hass)
using_multipan = await platform.async_using_multipan(hass)
_LOGGER.info(
"Registering new multipan platform '%s', using multipan: %s, channel: %s",
integration_domain,
using_multipan,
channel,
)
if self._channel is not None or not using_multipan:
return
new_channel = await platform.async_get_channel(hass)
if new_channel is None:
if channel is None:
return
_LOGGER.info(
"Setting multipan channel to %s (source: '%s')",
new_channel,
channel,
integration_domain,
)
self.async_set_channel(new_channel)
self.async_set_channel(channel)
async def async_change_channel(
self, channel: int, delay: float

View File

@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==2.6.3"],
"requirements": ["aiohomekit==2.6.5"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}

View File

@@ -120,7 +120,7 @@ class ImapMessage:
@property
def subject(self) -> str:
"""Decode the message subject."""
decoded_header = decode_header(self.email_message["Subject"])
decoded_header = decode_header(self.email_message["Subject"] or "")
subject_header = make_header(decoded_header)
return str(subject_header)

View File

@@ -302,12 +302,9 @@ class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity):
async def async_select_option(self, option: str) -> None:
"""Select new option."""
if option not in self.options:
_LOGGER.warning(
"Invalid option: %s (possible options: %s)",
option,
", ".join(self.options),
raise HomeAssistantError(
f"Invalid option: {option} (possible options: {', '.join(self.options)})"
)
return
self._attr_current_option = option
self.async_write_ha_state()

View File

@@ -25,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
SENSOR_TYPES = {
OPEN_CLOSE_SENSOR: BinarySensorDeviceClass.OPENING,
@@ -62,7 +62,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.BINARY_SENSOR}"
async_dispatcher_connect(hass, signal, async_add_insteon_binary_sensor_entities)
async_add_insteon_binary_sensor_entities()
async_add_insteon_devices(
hass,
Platform.BINARY_SENSOR,
InsteonBinarySensorEntity,
async_add_entities,
)
class InsteonBinarySensorEntity(InsteonEntity, BinarySensorEntity):

View File

@@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
FAN_ONLY = "fan_only"
@@ -71,7 +71,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.CLIMATE}"
async_dispatcher_connect(hass, signal, async_add_insteon_climate_entities)
async_add_insteon_climate_entities()
async_add_insteon_devices(
hass,
Platform.CLIMATE,
InsteonClimateEntity,
async_add_entities,
)
class InsteonClimateEntity(InsteonEntity, ClimateEntity):

View File

@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
async def async_setup_entry(
@@ -34,7 +34,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.COVER}"
async_dispatcher_connect(hass, signal, async_add_insteon_cover_entities)
async_add_insteon_cover_entities()
async_add_insteon_devices(
hass,
Platform.COVER,
InsteonCoverEntity,
async_add_entities,
)
class InsteonCoverEntity(InsteonEntity, CoverEntity):

View File

@@ -17,7 +17,7 @@ from homeassistant.util.percentage import (
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
SPEED_RANGE = (1, 255) # off is not included
@@ -38,7 +38,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.FAN}"
async_dispatcher_connect(hass, signal, async_add_insteon_fan_entities)
async_add_insteon_fan_entities()
async_add_insteon_devices(
hass,
Platform.FAN,
InsteonFanEntity,
async_add_entities,
)
class InsteonFanEntity(InsteonEntity, FanEntity):

View File

@@ -1,4 +1,7 @@
"""Utility methods for the Insteon platform."""
from collections.abc import Iterable
from pyinsteon.device_types.device_base import Device
from pyinsteon.device_types.ipdb import (
AccessControl_Morningstar,
ClimateControl_Thermostat,
@@ -44,7 +47,7 @@ from pyinsteon.device_types.ipdb import (
from homeassistant.const import Platform
DEVICE_PLATFORM = {
DEVICE_PLATFORM: dict[Device, dict[Platform, Iterable[int]]] = {
AccessControl_Morningstar: {Platform.LOCK: [1]},
DimmableLightingControl: {Platform.LIGHT: [1]},
DimmableLightingControl_Dial: {Platform.LIGHT: [1]},
@@ -101,11 +104,11 @@ DEVICE_PLATFORM = {
}
def get_device_platforms(device):
def get_device_platforms(device) -> dict[Platform, Iterable[int]]:
"""Return the HA platforms for a device type."""
return DEVICE_PLATFORM.get(type(device), {}).keys()
return DEVICE_PLATFORM.get(type(device), {})
def get_platform_groups(device, domain) -> dict:
"""Return the platforms that a device belongs in."""
return DEVICE_PLATFORM.get(type(device), {}).get(domain, {}) # type: ignore[attr-defined]
def get_device_platform_groups(device: Device, platform: Platform) -> Iterable[int]:
"""Return the list of device groups for a platform."""
return get_device_platforms(device).get(platform, [])

View File

@@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
MAX_BRIGHTNESS = 255
@@ -37,7 +37,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.LIGHT}"
async_dispatcher_connect(hass, signal, async_add_insteon_light_entities)
async_add_insteon_light_entities()
async_add_insteon_devices(
hass,
Platform.LIGHT,
InsteonDimmerEntity,
async_add_entities,
)
class InsteonDimmerEntity(InsteonEntity, LightEntity):

View File

@@ -11,7 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
async def async_setup_entry(
@@ -30,7 +30,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.LOCK}"
async_dispatcher_connect(hass, signal, async_add_insteon_lock_entities)
async_add_insteon_lock_entities()
async_add_insteon_devices(
hass,
Platform.LOCK,
InsteonLockEntity,
async_add_entities,
)
class InsteonLockEntity(InsteonEntity, LockEntity):

View File

@@ -17,7 +17,7 @@
"iot_class": "local_push",
"loggers": ["pyinsteon", "pypubsub"],
"requirements": [
"pyinsteon==1.4.2",
"pyinsteon==1.4.3",
"insteon-frontend-home-assistant==0.3.5"
],
"usb": [

View File

@@ -10,7 +10,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
async def async_setup_entry(
@@ -33,7 +33,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.SWITCH}"
async_dispatcher_connect(hass, signal, async_add_insteon_switch_entities)
async_add_insteon_switch_entities()
async_add_insteon_devices(
hass,
Platform.SWITCH,
InsteonSwitchEntity,
async_add_entities,
)
class InsteonSwitchEntity(InsteonEntity, SwitchEntity):

View File

@@ -1,7 +1,10 @@
"""Utilities used by insteon component."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
import logging
from typing import TYPE_CHECKING, Any
from pyinsteon import devices
from pyinsteon.address import Address
@@ -30,6 +33,7 @@ from homeassistant.const import (
CONF_ENTITY_ID,
CONF_PLATFORM,
ENTITY_MATCH_ALL,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import device_registry as dr
@@ -38,6 +42,7 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_send,
dispatcher_send,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
CONF_CAT,
@@ -78,7 +83,7 @@ from .const import (
SRV_X10_ALL_LIGHTS_ON,
SRV_X10_ALL_UNITS_OFF,
)
from .ipdb import get_device_platforms, get_platform_groups
from .ipdb import get_device_platform_groups, get_device_platforms
from .schemas import (
ADD_ALL_LINK_SCHEMA,
ADD_DEFAULT_LINKS_SCHEMA,
@@ -89,6 +94,9 @@ from .schemas import (
X10_HOUSECODE_SCHEMA,
)
if TYPE_CHECKING:
from .insteon_entity import InsteonEntity
_LOGGER = logging.getLogger(__name__)
@@ -132,6 +140,9 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
_LOGGER.debug("Firing event %s with %s", event, schema)
hass.bus.async_fire(event, schema)
if str(device.address).startswith("X10"):
return
for name_or_group, event in device.events.items():
if isinstance(name_or_group, int):
for _, event in device.events[name_or_group].items():
@@ -158,8 +169,10 @@ def register_new_device_callback(hass):
await device.async_status()
platforms = get_device_platforms(device)
for platform in platforms:
groups = get_device_platform_groups(device, platform)
signal = f"{SIGNAL_ADD_ENTITIES}_{platform}"
dispatcher_send(hass, signal, {"address": device.address})
dispatcher_send(hass, signal, {"address": device.address, "groups": groups})
add_insteon_events(hass, device)
devices.subscribe(async_new_insteon_device, force_strong_ref=True)
@@ -383,20 +396,38 @@ def print_aldb_to_log(aldb):
@callback
def async_add_insteon_entities(
hass, platform, entity_type, async_add_entities, discovery_info
):
"""Add Insteon devices to a platform."""
new_entities = []
device_list = [discovery_info.get("address")] if discovery_info else devices
for address in device_list:
device = devices[address]
groups = get_platform_groups(device, platform)
for group in groups:
new_entities.append(entity_type(device, group))
hass: HomeAssistant,
platform: Platform,
entity_type: type[InsteonEntity],
async_add_entities: AddEntitiesCallback,
discovery_info: dict[str, Any],
) -> None:
"""Add an Insteon group to a platform."""
address = discovery_info["address"]
device = devices[address]
new_entities = [
entity_type(device=device, group=group) for group in discovery_info["groups"]
]
async_add_entities(new_entities)
@callback
def async_add_insteon_devices(
hass: HomeAssistant,
platform: Platform,
entity_type: type[InsteonEntity],
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add all entities to a platform."""
for address in devices:
device = devices[address]
groups = get_device_platform_groups(device, platform)
discovery_info = {"address": address, "groups": groups}
async_add_insteon_entities(
hass, platform, entity_type, async_add_entities, discovery_info
)
def get_usb_ports() -> dict[str, str]:
"""Return a dict of USB ports and their friendly names."""
ports = list_ports.comports()

View File

@@ -1,16 +1,19 @@
"""Numeric integration of data coming from a source sensor over time."""
from __future__ import annotations
from decimal import Decimal, DecimalException
from dataclasses import dataclass
from decimal import Decimal, DecimalException, InvalidOperation
import logging
from typing import Final
from typing import Any, Final
from typing_extensions import Self
import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA,
RestoreSensor,
SensorDeviceClass,
SensorEntity,
SensorExtraStoredData,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
@@ -28,7 +31,6 @@ from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
@@ -79,6 +81,53 @@ PLATFORM_SCHEMA = vol.All(
)
@dataclass
class IntegrationSensorExtraStoredData(SensorExtraStoredData):
"""Object to hold extra stored data."""
source_entity: str | None
last_valid_state: Decimal | None
def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the utility sensor data."""
data = super().as_dict()
data["source_entity"] = self.source_entity
data["last_valid_state"] = (
str(self.last_valid_state) if self.last_valid_state else None
)
return data
@classmethod
def from_dict(cls, restored: dict[str, Any]) -> Self | None:
"""Initialize a stored sensor state from a dict."""
extra = SensorExtraStoredData.from_dict(restored)
if extra is None:
return None
source_entity = restored.get(ATTR_SOURCE_ID)
try:
last_valid_state = (
Decimal(str(restored.get("last_valid_state")))
if restored.get("last_valid_state")
else None
)
except InvalidOperation:
# last_period is corrupted
_LOGGER.error("Could not use last_valid_state")
return None
if last_valid_state is None:
return None
return cls(
extra.native_value,
extra.native_unit_of_measurement,
source_entity,
last_valid_state,
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
@@ -129,7 +178,7 @@ async def async_setup_platform(
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class IntegrationSensor(RestoreEntity, SensorEntity):
class IntegrationSensor(RestoreSensor):
"""Representation of an integration sensor."""
_attr_state_class = SensorStateClass.TOTAL
@@ -160,7 +209,8 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
self._unit_time = UNIT_TIME[unit_time]
self._unit_time_str = unit_time
self._attr_icon = "mdi:chart-histogram"
self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity}
self._source_entity: str = source_entity
self._last_valid_state: Decimal | None = None
def _unit(self, source_unit: str) -> str:
"""Derive unit from the source sensor, SI prefix and time unit."""
@@ -175,10 +225,28 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
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()) is not None:
if state.state == STATE_UNAVAILABLE:
self._attr_available = False
elif state.state != STATE_UNKNOWN:
if (last_sensor_data := await self.async_get_last_sensor_data()) is not None:
self._state = (
Decimal(str(last_sensor_data.native_value))
if last_sensor_data.native_value
else last_sensor_data.last_valid_state
)
self._attr_native_value = last_sensor_data.native_value
self._unit_of_measurement = last_sensor_data.native_unit_of_measurement
self._last_valid_state = last_sensor_data.last_valid_state
_LOGGER.debug(
"Restored state %s and last_valid_state %s",
self._state,
self._last_valid_state,
)
elif (state := await self.async_get_last_state()) is not None:
# legacy to be removed on 2023.10 (we are keeping this to avoid losing data during the transition)
if state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]:
if state.state == STATE_UNAVAILABLE:
self._attr_available = False
else:
try:
self._state = Decimal(state.state)
except (DecimalException, ValueError) as err:
@@ -295,6 +363,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
self._state += integral
else:
self._state = integral
self._last_valid_state = self._state
self.async_write_ha_state()
self.async_on_remove(
@@ -314,3 +383,33 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
def native_unit_of_measurement(self) -> str | None:
"""Return the unit the value is expressed in."""
return self._unit_of_measurement
@property
def extra_state_attributes(self) -> dict[str, str] | None:
"""Return the state attributes of the sensor."""
state_attr = {
ATTR_SOURCE_ID: self._source_entity,
}
return state_attr
@property
def extra_restore_state_data(self) -> IntegrationSensorExtraStoredData:
"""Return sensor specific state data to be restored."""
return IntegrationSensorExtraStoredData(
self.native_value,
self.native_unit_of_measurement,
self._source_entity,
self._last_valid_state,
)
async def async_get_last_sensor_data(
self,
) -> IntegrationSensorExtraStoredData | None:
"""Restore Utility Meter Sensor Extra Stored Data."""
if (restored_last_extra_data := await self.async_get_last_extra_data()) is None:
return None
return IntegrationSensorExtraStoredData.from_dict(
restored_last_extra_data.as_dict()
)

View File

@@ -19,21 +19,18 @@ PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up IPP from a config entry."""
hass.data.setdefault(DOMAIN, {})
if not (coordinator := hass.data[DOMAIN].get(entry.entry_id)):
# Create IPP instance for this entry
coordinator = IPPDataUpdateCoordinator(
hass,
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
base_path=entry.data[CONF_BASE_PATH],
tls=entry.data[CONF_SSL],
verify_ssl=entry.data[CONF_VERIFY_SSL],
)
hass.data[DOMAIN][entry.entry_id] = coordinator
coordinator = IPPDataUpdateCoordinator(
hass,
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
base_path=entry.data[CONF_BASE_PATH],
tls=entry.data[CONF_SSL],
verify_ssl=entry.data[CONF_VERIFY_SSL],
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -41,7 +38,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -8,6 +8,6 @@
"iot_class": "local_polling",
"loggers": ["deepmerge", "pyipp"],
"quality_scale": "platinum",
"requirements": ["pyipp==0.12.1"],
"requirements": ["pyipp==0.13.0"],
"zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."]
}

View File

@@ -1,11 +1,14 @@
"""Receive signals from a keyboard and use it as a remote control."""
# pylint: disable=import-error
from __future__ import annotations
import asyncio
from contextlib import suppress
import logging
import os
from typing import Any
import aionotify
from asyncinotify import Inotify, Mask
from evdev import InputDevice, categorize, ecodes, list_devices
import voluptuous as vol
@@ -64,9 +67,9 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the keyboard_remote."""
config = config[DOMAIN]
domain_config: list[dict[str, Any]] = config[DOMAIN]
remote = KeyboardRemote(hass, config)
remote = KeyboardRemote(hass, domain_config)
remote.setup()
return True
@@ -75,12 +78,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
class KeyboardRemote:
"""Manage device connection/disconnection using inotify to asynchronously monitor."""
def __init__(self, hass, config):
def __init__(self, hass: HomeAssistant, config: list[dict[str, Any]]) -> None:
"""Create handlers and setup dictionaries to keep track of them."""
self.hass = hass
self.handlers_by_name = {}
self.handlers_by_descriptor = {}
self.active_handlers_by_descriptor = {}
self.active_handlers_by_descriptor: dict[str, asyncio.Future] = {}
self.inotify = None
self.watcher = None
self.monitor_task = None
@@ -110,16 +114,12 @@ class KeyboardRemote:
connected, and start monitoring for device connection/disconnection.
"""
# start watching
self.watcher = aionotify.Watcher()
self.watcher.watch(
alias="devinput",
path=DEVINPUT,
flags=aionotify.Flags.CREATE
| aionotify.Flags.ATTRIB
| aionotify.Flags.DELETE,
_LOGGER.debug("Start monitoring")
self.inotify = Inotify()
self.watcher = self.inotify.add_watch(
DEVINPUT, Mask.CREATE | Mask.ATTRIB | Mask.DELETE
)
await self.watcher.setup(self.hass.loop)
# add initial devices (do this AFTER starting watcher in order to
# avoid race conditions leading to missing device connections)
@@ -134,7 +134,9 @@ class KeyboardRemote:
continue
self.active_handlers_by_descriptor[descriptor] = handler
initial_start_monitoring.add(handler.async_start_monitoring(dev))
initial_start_monitoring.add(
asyncio.create_task(handler.async_device_start_monitoring(dev))
)
if initial_start_monitoring:
await asyncio.wait(initial_start_monitoring)
@@ -146,6 +148,10 @@ class KeyboardRemote:
_LOGGER.debug("Cleanup on shutdown")
if self.inotify and self.watcher:
self.inotify.rm_watch(self.watcher)
self.watcher = None
if self.monitor_task is not None:
if not self.monitor_task.done():
self.monitor_task.cancel()
@@ -153,11 +159,16 @@ class KeyboardRemote:
handler_stop_monitoring = set()
for handler in self.active_handlers_by_descriptor.values():
handler_stop_monitoring.add(handler.async_stop_monitoring())
handler_stop_monitoring.add(
asyncio.create_task(handler.async_device_stop_monitoring())
)
if handler_stop_monitoring:
await asyncio.wait(handler_stop_monitoring)
if self.inotify:
self.inotify.close()
self.inotify = None
def get_device_handler(self, descriptor):
"""Find the correct device handler given a descriptor (path)."""
@@ -187,20 +198,21 @@ class KeyboardRemote:
async def async_monitor_devices(self):
"""Monitor asynchronously for device connection/disconnection or permissions changes."""
_LOGGER.debug("Start monitoring loop")
try:
while True:
event = await self.watcher.get_event()
async for event in self.inotify:
descriptor = f"{DEVINPUT}/{event.name}"
_LOGGER.debug("got events for %s: %s", descriptor, event.mask)
descriptor_active = descriptor in self.active_handlers_by_descriptor
if (event.flags & aionotify.Flags.DELETE) and descriptor_active:
if (event.mask & Mask.DELETE) and descriptor_active:
handler = self.active_handlers_by_descriptor[descriptor]
del self.active_handlers_by_descriptor[descriptor]
await handler.async_stop_monitoring()
await handler.async_device_stop_monitoring()
elif (
(event.flags & aionotify.Flags.CREATE)
or (event.flags & aionotify.Flags.ATTRIB)
(event.mask & Mask.CREATE) or (event.mask & Mask.ATTRIB)
) and not descriptor_active:
dev, handler = await self.hass.async_add_executor_job(
self.get_device_handler, descriptor
@@ -208,31 +220,32 @@ class KeyboardRemote:
if handler is None:
continue
self.active_handlers_by_descriptor[descriptor] = handler
await handler.async_start_monitoring(dev)
await handler.async_device_start_monitoring(dev)
except asyncio.CancelledError:
_LOGGER.debug("Monitoring canceled")
return
class DeviceHandler:
"""Manage input events using evdev with asyncio."""
def __init__(self, hass, dev_block):
def __init__(self, hass: HomeAssistant, dev_block: dict[str, Any]) -> None:
"""Fill configuration data."""
self.hass = hass
key_types = dev_block.get(TYPE)
key_types = dev_block[TYPE]
self.key_values = set()
for key_type in key_types:
self.key_values.add(KEY_VALUE[key_type])
self.emulate_key_hold = dev_block.get(EMULATE_KEY_HOLD)
self.emulate_key_hold_delay = dev_block.get(EMULATE_KEY_HOLD_DELAY)
self.emulate_key_hold_repeat = dev_block.get(EMULATE_KEY_HOLD_REPEAT)
self.emulate_key_hold = dev_block[EMULATE_KEY_HOLD]
self.emulate_key_hold_delay = dev_block[EMULATE_KEY_HOLD_DELAY]
self.emulate_key_hold_repeat = dev_block[EMULATE_KEY_HOLD_REPEAT]
self.monitor_task = None
self.dev = None
async def async_keyrepeat(self, path, name, code, delay, repeat):
async def async_device_keyrepeat(self, path, name, code, delay, repeat):
"""Emulate keyboard delay/repeat behaviour by sending key events on a timer."""
await asyncio.sleep(delay)
@@ -248,8 +261,9 @@ class KeyboardRemote:
)
await asyncio.sleep(repeat)
async def async_start_monitoring(self, dev):
async def async_device_start_monitoring(self, dev):
"""Start event monitoring task and issue event."""
_LOGGER.debug("Keyboard async_device_start_monitoring, %s", dev.name)
if self.monitor_task is None:
self.dev = dev
self.monitor_task = self.hass.async_create_task(
@@ -261,7 +275,7 @@ class KeyboardRemote:
)
_LOGGER.debug("Keyboard (re-)connected, %s", dev.name)
async def async_stop_monitoring(self):
async def async_device_stop_monitoring(self):
"""Stop event monitoring task and issue event."""
if self.monitor_task is not None:
with suppress(OSError):
@@ -295,6 +309,7 @@ class KeyboardRemote:
_LOGGER.debug("Start device monitoring")
await self.hass.async_add_executor_job(dev.grab)
async for event in dev.async_read_loop():
# pylint: disable=no-member
if event.type is ecodes.EV_KEY:
if event.value in self.key_values:
_LOGGER.debug(categorize(event))
@@ -313,7 +328,7 @@ class KeyboardRemote:
and self.emulate_key_hold
):
repeat_tasks[event.code] = self.hass.async_create_task(
self.async_keyrepeat(
self.async_device_keyrepeat(
dev.path,
dev.name,
event.code,

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/keyboard_remote",
"iot_class": "local_push",
"loggers": ["aionotify", "evdev"],
"requirements": ["evdev==1.4.0", "aionotify==0.2.0"]
"requirements": ["evdev==1.6.1", "asyncinotify==4.0.2"]
}

View File

@@ -12,7 +12,7 @@
"quality_scale": "platinum",
"requirements": [
"xknx==2.10.0",
"xknxproject==3.1.0",
"knx_frontend==2023.5.31.141540"
"xknxproject==3.1.1",
"knx-frontend==2023.6.9.195839"
]
}

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Final
from knx_frontend import get_build_id, locate_dir
from knx_frontend import entrypoint_js, is_dev_build, locate_dir
import voluptuous as vol
from xknx.telegram import TelegramDirection
from xknxproject.exceptions import XknxProjectException
@@ -31,9 +31,10 @@ async def register_panel(hass: HomeAssistant) -> None:
if DOMAIN not in hass.data.get("frontend_panels", {}):
path = locate_dir()
build_id = get_build_id()
hass.http.register_static_path(
URL_BASE, path, cache_headers=(build_id != "dev")
URL_BASE,
path,
cache_headers=not is_dev_build,
)
await panel_custom.async_register_panel(
hass=hass,
@@ -41,12 +42,13 @@ async def register_panel(hass: HomeAssistant) -> None:
webcomponent_name="knx-frontend",
sidebar_title=DOMAIN.upper(),
sidebar_icon="mdi:bus-electric",
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
module_url=f"{URL_BASE}/{entrypoint_js()}",
embed_iframe=True,
require_admin=True,
)
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "knx/info",
@@ -129,6 +131,7 @@ async def ws_project_file_remove(
connection.send_result(msg["id"])
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "knx/group_monitor_info",
@@ -155,6 +158,7 @@ def ws_group_monitor_info(
)
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "knx/subscribe_telegrams",

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from pylast import LastFMNetwork, User, WSError
from pylast import LastFMNetwork, PyLastError, User, WSError
import voluptuous as vol
from homeassistant.config_entries import (
@@ -128,11 +128,14 @@ class LastFmConfigFlowHandler(ConfigFlow, domain=DOMAIN):
main_user, _ = get_lastfm_user(
self.data[CONF_API_KEY], self.data[CONF_MAIN_USER]
)
friends_response = await self.hass.async_add_executor_job(
main_user.get_friends
)
friends = [
SelectOptionDict(value=friend.name, label=friend.get_name(True))
for friend in main_user.get_friends()
for friend in friends_response
]
except WSError:
except PyLastError:
friends = []
return self.async_show_form(
step_id="friends",
@@ -197,11 +200,14 @@ class LastFmOptionsFlowHandler(OptionsFlowWithConfigEntry):
self.options[CONF_API_KEY],
self.options[CONF_MAIN_USER],
)
friends_response = await self.hass.async_add_executor_job(
main_user.get_friends
)
friends = [
SelectOptionDict(value=friend.name, label=friend.get_name(True))
for friend in main_user.get_friends()
for friend in friends_response
]
except WSError:
except PyLastError:
friends = []
else:
friends = []

View File

@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pylitterbot"],
"requirements": ["pylitterbot==2023.4.0"]
"requirements": ["pylitterbot==2023.4.2"]
}

View File

@@ -13,7 +13,6 @@ from homeassistant.helpers.typing import ConfigType
from . import websocket_api
from .const import (
ATTR_LEVEL,
DEFAULT_LOGSEVERITY,
DOMAIN,
LOGGER_DEFAULT,
LOGGER_FILTERS,
@@ -39,9 +38,7 @@ CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(
LOGGER_DEFAULT, default=DEFAULT_LOGSEVERITY
): _VALID_LOG_LEVEL,
vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL,
vol.Optional(LOGGER_LOGS): vol.Schema({cv.string: _VALID_LOG_LEVEL}),
vol.Optional(LOGGER_FILTERS): vol.Schema({cv.string: [cv.is_regex]}),
}

View File

@@ -119,7 +119,7 @@ class LoggerSettings:
self._yaml_config = yaml_config
self._default_level = logging.INFO
if DOMAIN in yaml_config:
if DOMAIN in yaml_config and LOGGER_DEFAULT in yaml_config[DOMAIN]:
self._default_level = yaml_config[DOMAIN][LOGGER_DEFAULT]
self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store(
hass, STORAGE_VERSION, STORAGE_KEY

View File

@@ -38,7 +38,7 @@ def async_setup(hass: HomeAssistant) -> None:
class LocalSource(MediaSource):
"""Provide local directories as media sources."""
name: str = "Local Media"
name: str = "My media"
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize local source."""

View File

@@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/melnor",
"iot_class": "local_polling",
"requirements": ["melnor-bluetooth==0.0.22"]
"requirements": ["melnor-bluetooth==0.0.25"]
}

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/netdata",
"iot_class": "local_polling",
"loggers": ["netdata"],
"requirements": ["netdata==1.0.1"]
"requirements": ["netdata==1.1.0"]
}

View File

@@ -66,7 +66,7 @@ async def async_setup_platform(
port = config[CONF_PORT]
resources = config[CONF_RESOURCES]
netdata = NetdataData(Netdata(host, port=port))
netdata = NetdataData(Netdata(host, port=port, timeout=20.0))
await netdata.async_update()
if netdata.api.metrics is None:

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/noaa_tides",
"iot_class": "cloud_polling",
"loggers": ["noaa_coops"],
"requirements": ["noaa-coops==0.1.8"]
"requirements": ["noaa-coops==0.1.9"]
}

View File

@@ -12,5 +12,5 @@
"documentation": "https://www.home-assistant.io/integrations/nuki",
"iot_class": "local_polling",
"loggers": ["pynuki"],
"requirements": ["pynuki==1.6.1"]
"requirements": ["pynuki==1.6.2"]
}

View File

@@ -4,5 +4,5 @@
"codeowners": ["@joostlek"],
"documentation": "https://www.home-assistant.io/integrations/opensky",
"iot_class": "cloud_polling",
"requirements": ["python-opensky==0.0.7"]
"requirements": ["python-opensky==0.0.9"]
}

View File

@@ -78,7 +78,7 @@ def setup_platform(
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
radius = config.get(CONF_RADIUS, 0)
bounding_box = OpenSky.get_bounding_box(latitude, longitude, radius)
bounding_box = OpenSky.get_bounding_box(latitude, longitude, radius * 1000)
session = async_get_clientsession(hass)
opensky = OpenSky(session=session)
add_entities(

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/opple",
"iot_class": "local_polling",
"loggers": ["pyoppleio"],
"requirements": ["pyoppleio==1.0.5"]
"requirements": ["pyoppleio-legacy==1.0.8"]
}

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/otbr",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.1.0"]
"requirements": ["python-otbr-api==2.2.0"]
}

View File

@@ -95,6 +95,11 @@ class OTBRData:
"""Create an active operational dataset."""
return await self.api.create_active_dataset(dataset)
@_handle_otbr_error
async def delete_active_dataset(self) -> None:
"""Delete the active operational dataset."""
return await self.api.delete_active_dataset()
@_handle_otbr_error
async def set_active_dataset_tlvs(self, dataset: bytes) -> None:
"""Set current active operational dataset in TLVS format."""

View File

@@ -81,6 +81,12 @@ async def websocket_create_network(
connection.send_error(msg["id"], "set_enabled_failed", str(exc))
return
try:
await data.delete_active_dataset()
except HomeAssistantError as exc:
connection.send_error(msg["id"], "delete_active_dataset_failed", str(exc))
return
try:
await data.create_active_dataset(
python_otbr_api.ActiveDataSet(

View File

@@ -13,7 +13,7 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
"requirements": ["pyoverkiz==1.7.9"],
"requirements": ["pyoverkiz==1.8.0"],
"zeroconf": [
{
"type": "_kizbox._tcp.local.",

View File

@@ -29,8 +29,6 @@ ATTR_NOTIFICATION_ID: Final = "notification_id"
ATTR_TITLE: Final = "title"
ATTR_STATUS: Final = "status"
STATUS_UNREAD = "unread"
STATUS_READ = "read"
# Remove EVENT_PERSISTENT_NOTIFICATIONS_UPDATED in Home Assistant 2023.9
EVENT_PERSISTENT_NOTIFICATIONS_UPDATED = "persistent_notifications_updated"
@@ -43,7 +41,6 @@ class Notification(TypedDict):
message: str
notification_id: str
title: str | None
status: str
class UpdateType(StrEnum):
@@ -98,7 +95,6 @@ def async_create(
notifications[notification_id] = {
ATTR_MESSAGE: message,
ATTR_NOTIFICATION_ID: notification_id,
ATTR_STATUS: STATUS_UNREAD,
ATTR_TITLE: title,
ATTR_CREATED_AT: dt_util.utcnow(),
}
@@ -135,7 +131,6 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the persistent notification component."""
notifications = _async_get_or_create_notifications(hass)
@callback
def create_service(call: ServiceCall) -> None:
@@ -152,29 +147,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Handle the dismiss notification service call."""
async_dismiss(hass, call.data[ATTR_NOTIFICATION_ID])
@callback
def mark_read_service(call: ServiceCall) -> None:
"""Handle the mark_read notification service call."""
notification_id = call.data.get(ATTR_NOTIFICATION_ID)
if notification_id not in notifications:
_LOGGER.error(
(
"Marking persistent_notification read failed: "
"Notification ID %s not found"
),
notification_id,
)
return
notification = notifications[notification_id]
notification[ATTR_STATUS] = STATUS_READ
async_dispatcher_send(
hass,
SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED,
UpdateType.UPDATED,
{notification_id: notification},
)
hass.services.async_register(
DOMAIN,
"create",
@@ -192,10 +164,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
DOMAIN, "dismiss", dismiss_service, SCHEMA_SERVICE_NOTIFICATION
)
hass.services.async_register(
DOMAIN, "mark_read", mark_read_service, SCHEMA_SERVICE_NOTIFICATION
)
websocket_api.async_register_command(hass, websocket_get_notifications)
websocket_api.async_register_command(hass, websocket_subscribe_notifications)

View File

@@ -33,15 +33,3 @@ dismiss:
example: 1234
selector:
text:
mark_read:
name: Mark read
description: Mark a notification read.
fields:
notification_id:
name: Notification ID
description: Target ID of the notification, which should be mark read.
required: true
example: 1234
selector:
text:

View File

@@ -4,5 +4,5 @@
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback",
"iot_class": "local_polling",
"requirements": ["pulsectl==20.2.4"]
"requirements": ["pulsectl==23.5.2"]
}

View File

@@ -10,7 +10,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["regenmaschine"],
"requirements": ["regenmaschine==2023.05.1"],
"requirements": ["regenmaschine==2023.06.0"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@@ -0,0 +1,38 @@
"""Support for the Airzone diagnostics."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics.util import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import RoborockDataUpdateCoordinator
TO_REDACT_CONFIG = ["token", "sn", "rruid", CONF_UNIQUE_ID, "username", "uid"]
TO_REDACT_COORD = ["duid", "localKey", "mac", "bssid"]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
config_entry.entry_id
]
return {
"config_entry": async_redact_data(config_entry.data, TO_REDACT_CONFIG),
"coordinators": {
f"**REDACTED-{i}**": {
"roborock_device_info": async_redact_data(
coordinator.roborock_device_info.as_dict(), TO_REDACT_COORD
),
"api": coordinator.api.diagnostic_data,
}
for i, coordinator in enumerate(coordinators.values())
},
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/roborock",
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": ["python-roborock==0.17.0"]
"requirements": ["python-roborock==0.23.4"]
}

View File

@@ -1,5 +1,6 @@
"""Roborock Models."""
from dataclasses import dataclass
from typing import Any
from roborock.containers import HomeDataDevice, HomeDataProduct, NetworkInfo
from roborock.roborock_typing import DeviceProp
@@ -13,3 +14,12 @@ class RoborockHassDeviceInfo:
network_info: NetworkInfo
product: HomeDataProduct
props: DeviceProp
def as_dict(self) -> dict[str, dict[str, Any]]:
"""Turn RoborockHassDeviceInfo into a dictionary."""
return {
"device": self.device.as_dict(),
"network_info": self.network_info.as_dict(),
"product": self.product.as_dict(),
"props": self.props.as_dict(),
}

View File

@@ -90,8 +90,11 @@
"name": "Mop intensity",
"state": {
"off": "Off",
"low": "Low",
"mild": "Mild",
"medium": "Medium",
"moderate": "Moderate",
"high": "High",
"intense": "Intense",
"custom": "Custom"
}

View File

@@ -1,9 +1,11 @@
"""Support for Roborock switch."""
import asyncio
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
import logging
from typing import Any
from roborock.exceptions import RoborockException
from roborock.roborock_typing import RoborockCommand
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
@@ -30,6 +32,8 @@ class RoborockSwitchDescriptionMixin:
evaluate_value: Callable[[dict], bool]
# Sets the status of the switch
set_command: Callable[[RoborockEntity, bool], Coroutine[Any, Any, dict]]
# Check support of this feature
check_support: Callable[[RoborockDataUpdateCoordinator], Coroutine[Any, Any, dict]]
@dataclass
@@ -45,6 +49,9 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [
RoborockCommand.SET_CHILD_LOCK_STATUS, {"lock_status": 1 if value else 0}
),
get_value=lambda data: data.send(RoborockCommand.GET_CHILD_LOCK_STATUS),
check_support=lambda data: data.api.send_command(
RoborockCommand.GET_CHILD_LOCK_STATUS
),
evaluate_value=lambda data: data["lock_status"] == 1,
key="child_lock",
translation_key="child_lock",
@@ -56,6 +63,9 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [
RoborockCommand.SET_FLOW_LED_STATUS, {"status": 1 if value else 0}
),
get_value=lambda data: data.send(RoborockCommand.GET_FLOW_LED_STATUS),
check_support=lambda data: data.api.send_command(
RoborockCommand.GET_FLOW_LED_STATUS
),
evaluate_value=lambda data: data["status"] == 1,
key="status_indicator",
translation_key="status_indicator",
@@ -75,16 +85,38 @@ async def async_setup_entry(
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
config_entry.entry_id
]
async_add_entities(
(
RoborockSwitchEntity(
f"{description.key}_{slugify(device_id)}",
coordinator,
description,
)
for device_id, coordinator in coordinators.items()
for description in SWITCH_DESCRIPTIONS
possible_entities: list[
tuple[str, RoborockDataUpdateCoordinator, RoborockSwitchDescription]
] = [
(device_id, coordinator, description)
for device_id, coordinator in coordinators.items()
for description in SWITCH_DESCRIPTIONS
]
# We need to check if this function is supported by the device.
results = await asyncio.gather(
*(
description.check_support(coordinator)
for _, coordinator, description in possible_entities
),
return_exceptions=True,
)
valid_entities: list[RoborockSwitchEntity] = []
for posible_entity, result in zip(possible_entities, results):
if isinstance(result, Exception):
if not isinstance(result, RoborockException):
raise result
_LOGGER.debug("Not adding entity because of %s", result)
else:
valid_entities.append(
RoborockSwitchEntity(
f"{posible_entity[2].key}_{slugify(posible_entity[0])}",
posible_entity[1],
posible_entity[2],
result,
)
)
async_add_entities(
valid_entities,
True,
)
@@ -99,10 +131,12 @@ class RoborockSwitchEntity(RoborockEntity, SwitchEntity):
unique_id: str,
coordinator: RoborockDataUpdateCoordinator,
entity_description: RoborockSwitchDescription,
initial_value: bool,
) -> None:
"""Create a switch entity."""
self.entity_description = entity_description
super().__init__(unique_id, coordinator.device_info, coordinator.api)
self._attr_is_on = initial_value
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the switch."""

View File

@@ -22,13 +22,11 @@ PLATFORMS = [
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Roku from a config entry."""
hass.data.setdefault(DOMAIN, {})
if not (coordinator := hass.data[DOMAIN].get(entry.entry_id)):
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
hass.data[DOMAIN][entry.entry_id] = coordinator
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -36,7 +34,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/russound_rio",
"iot_class": "local_push",
"loggers": ["russound_rio"],
"requirements": ["russound_rio==0.1.8"]
"requirements": ["russound-rio==1.0.0"]
}

View File

@@ -535,7 +535,10 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
async def shutdown(self) -> None:
"""Shutdown the coordinator."""
if self.device.connected:
await async_stop_scanner(self.device)
try:
await async_stop_scanner(self.device)
except InvalidAuthError:
self.entry.async_start_reauth(self.hass)
await self.device.shutdown()
await self._async_disconnected()

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/sisyphus",
"iot_class": "local_push",
"loggers": ["sisyphus_control"],
"requirements": ["sisyphus-control==3.1.2"]
"requirements": ["sisyphus-control==3.1.3"]
}

View File

@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/thread",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.1.0", "pyroute2==0.7.5"],
"requirements": ["python-otbr-api==2.2.0", "pyroute2==0.7.5"],
"zeroconf": ["_meshcop._udp.local."]
}

View File

@@ -606,7 +606,7 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
)
last_stats = await get_instance(self.hass).async_add_executor_job(
get_last_statistics, self.hass, 1, statistic_id, True, {}
get_last_statistics, self.hass, 1, statistic_id, True, set()
)
if not last_stats:

View File

@@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["aiounifi"],
"quality_scale": "platinum",
"requirements": ["aiounifi==47"],
"requirements": ["aiounifi==48"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@@ -14,8 +14,6 @@ from pyunifiprotect.data import (
ProtectAdoptableDeviceModel,
ProtectModelWithId,
Sensor,
SmartDetectAudioType,
SmartDetectObjectType,
)
from pyunifiprotect.data.nvr import UOSDisk
@@ -364,8 +362,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_value="is_smart_detected",
ufp_required_field="can_detect_person",
ufp_enabled="is_person_detection_on",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.PERSON,
ufp_event_obj="last_person_detect_event",
),
ProtectBinaryEventEntityDescription(
key="smart_obj_vehicle",
@@ -374,8 +371,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_value="is_smart_detected",
ufp_required_field="can_detect_vehicle",
ufp_enabled="is_vehicle_detection_on",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.VEHICLE,
ufp_event_obj="last_vehicle_detect_event",
),
ProtectBinaryEventEntityDescription(
key="smart_obj_face",
@@ -384,8 +380,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_value="is_smart_detected",
ufp_required_field="can_detect_face",
ufp_enabled="is_face_detection_on",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.FACE,
ufp_event_obj="last_face_detect_event",
),
ProtectBinaryEventEntityDescription(
key="smart_obj_package",
@@ -394,8 +389,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_value="is_smart_detected",
ufp_required_field="can_detect_package",
ufp_enabled="is_package_detection_on",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.PACKAGE,
ufp_event_obj="last_package_detect_event",
),
ProtectBinaryEventEntityDescription(
key="smart_audio_any",
@@ -412,8 +406,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_value="is_smart_detected",
ufp_required_field="can_detect_smoke",
ufp_enabled="is_smoke_detection_on",
ufp_event_obj="last_smart_audio_detect_event",
ufp_smart_type=SmartDetectAudioType.SMOKE,
ufp_event_obj="last_smoke_detect_event",
),
ProtectBinaryEventEntityDescription(
key="smart_audio_cmonx",
@@ -422,8 +415,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_value="is_smart_detected",
ufp_required_field="can_detect_smoke",
ufp_enabled="is_smoke_detection_on",
ufp_event_obj="last_smart_audio_detect_event",
ufp_smart_type=SmartDetectAudioType.CMONX,
ufp_event_obj="last_cmonx_detect_event",
),
)

View File

@@ -40,6 +40,11 @@ from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type
_LOGGER = logging.getLogger(__name__)
ProtectDeviceType = ProtectAdoptableDeviceModel | NVR
SMART_EVENTS = {
EventType.SMART_DETECT,
EventType.SMART_AUDIO_DETECT,
EventType.SMART_DETECT_LINE,
}
@callback
@@ -223,6 +228,25 @@ class ProtectData:
# trigger updates for camera that the event references
elif isinstance(obj, Event):
if obj.type in SMART_EVENTS:
if obj.camera is not None:
if obj.end is None:
_LOGGER.debug(
"%s (%s): New smart detection started for %s (%s)",
obj.camera.name,
obj.camera.mac,
obj.smart_detect_types,
obj.id,
)
else:
_LOGGER.debug(
"%s (%s): Smart detection ended for %s (%s)",
obj.camera.name,
obj.camera.mac,
obj.smart_detect_types,
obj.id,
)
if obj.type == EventType.DEVICE_ADOPTED:
if obj.metadata is not None and obj.metadata.device_id is not None:
device = self.api.bootstrap.get_device_from_id(

View File

@@ -41,7 +41,7 @@
"iot_class": "local_push",
"loggers": ["pyunifiprotect", "unifi_discovery"],
"quality_scale": "platinum",
"requirements": ["pyunifiprotect==4.9.1", "unifi-discovery==1.1.7"],
"requirements": ["pyunifiprotect==4.10.3", "unifi-discovery==1.1.7"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
import logging
from typing import Any, Generic, TypeVar, cast
@@ -10,6 +11,7 @@ from typing import Any, Generic, TypeVar, cast
from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel
from homeassistant.helpers.entity import EntityDescription
from homeassistant.util import dt as dt_util
from .utils import get_nested_attr
@@ -67,7 +69,6 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]):
"""Mixin for events."""
ufp_event_obj: str | None = None
ufp_smart_type: str | None = None
def get_event_obj(self, obj: T) -> Event | None:
"""Return value from UniFi Protect device."""
@@ -79,23 +80,22 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]):
def get_is_on(self, obj: T) -> bool:
"""Return value if event is active."""
value = bool(self.get_ufp_value(obj))
if value:
event = self.get_event_obj(obj)
value = event is not None
if not value:
_LOGGER.debug("%s (%s): missing event", self.name, obj.mac)
event = self.get_event_obj(obj)
if event is None:
return False
if event is not None and self.ufp_smart_type is not None:
value = self.ufp_smart_type in event.smart_detect_types
if not value:
_LOGGER.debug(
"%s (%s): %s not in %s",
self.name,
obj.mac,
self.ufp_smart_type,
event.smart_detect_types,
)
now = dt_util.utcnow()
value = now > event.start
if value and event.end is not None and now > event.end:
value = False
# only log if the recent ended recently
if event.end + timedelta(seconds=10) < now:
_LOGGER.debug(
"%s (%s): end ended at %s",
self.name,
obj.mac,
event.end.isoformat(),
)
if value:
_LOGGER.debug("%s (%s): value is on", self.name, obj.mac)

View File

@@ -15,7 +15,6 @@ from pyunifiprotect.data import (
ProtectDeviceModel,
ProtectModelWithId,
Sensor,
SmartDetectObjectType,
)
from homeassistant.components.sensor import (
@@ -528,10 +527,9 @@ EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = (
name="License Plate Detected",
icon="mdi:car",
translation_key="license_plate",
ufp_smart_type=SmartDetectObjectType.LICENSE_PLATE,
ufp_value="is_smart_detected",
ufp_required_field="can_detect_license_plate",
ufp_event_obj="last_smart_detect_event",
ufp_event_obj="last_license_plate_detect_event",
),
)
@@ -767,8 +765,7 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity):
EventEntityMixin._async_update_device_from_protect(self, device)
is_on = self.entity_description.get_is_on(device)
is_license_plate = (
self.entity_description.ufp_smart_type
== SmartDetectObjectType.LICENSE_PLATE
self.entity_description.ufp_event_obj == "last_license_plate_detect_event"
)
if (
not is_on

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/waqi",
"iot_class": "cloud_polling",
"loggers": ["waqiasync"],
"requirements": ["waqiasync==1.0.0"]
"requirements": ["waqiasync==1.1.0"]
}

View File

@@ -16,5 +16,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble",
"iot_class": "local_push",
"requirements": ["xiaomi-ble==0.17.0"]
"requirements": ["xiaomi-ble==0.17.2"]
}

View File

@@ -275,9 +275,7 @@ async def async_setup_entry(
if not entity_method:
continue
await entity_method(**params)
update_tasks.append(
hass.async_create_task(entity.async_update_ha_state(True))
)
update_tasks.append(asyncio.create_task(entity.async_update_ha_state(True)))
if update_tasks:
await asyncio.wait(update_tasks)

View File

@@ -229,7 +229,9 @@ async def async_setup_entry(
if not hasattr(target_device, method["method"]):
continue
await getattr(target_device, method["method"])(**params)
update_tasks.append(target_device.async_update_ha_state(True))
update_tasks.append(
asyncio.create_task(target_device.async_update_ha_state(True))
)
if update_tasks:
await asyncio.wait(update_tasks)

View File

@@ -441,7 +441,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity):
return await self._try_command(
"Setting delay off miio device failed.",
self._device.delay_off,
delay_off_countdown * 60,
delay_off_countdown,
)
async def async_set_led_brightness_level(self, level: int) -> bool:

View File

@@ -500,7 +500,9 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities):
if not hasattr(device, method["method"]):
continue
await getattr(device, method["method"])(**params)
update_tasks.append(device.async_update_ha_state(True))
update_tasks.append(
asyncio.create_task(device.async_update_ha_state(True))
)
if update_tasks:
await asyncio.wait(update_tasks)

View File

@@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"iot_class": "local_push",
"requirements": ["yalexs-ble==2.1.17"]
"requirements": ["yalexs-ble==2.1.18"]
}

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