Compare commits

...

105 Commits

Author SHA1 Message Date
Paulus Schoutsen
dbd8ffc282 2023.2.1 (#87221) 2023-02-02 20:52:38 -05:00
Paulus Schoutsen
372afc5c28 Bumped version to 2023.2.1 2023-02-02 16:48:09 -05:00
Karlie Meads
ed8a0ef0ea Fix disabled condition within an automation action (#87213)
fixes undefined
2023-02-02 16:48:04 -05:00
Michael
1d8f5b2e16 Bump py-synologydsm-api to 2.1.1 (#87211)
bump py-synologydsm-api to 2.1.1
2023-02-02 16:48:03 -05:00
Bram Kragten
75796e1f0f Update frontend to 20230202.0 (#87208) 2023-02-02 16:48:02 -05:00
mkmer
063bbe91d1 Bump AIOSomecomfort to 0.0.6 (#87203)
* Bump to 0.0.5

* Bump aiosomecomfort to 0.0.6

* lower case aiosomecomfort

* Fix other bad imports....

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-02-02 16:48:01 -05:00
mkmer
bfcae4e07b Add Reauth config flow to honeywell (#86170) 2023-02-02 16:48:00 -05:00
J. Nick Koston
be77d7daa5 Fix statistics graphs not loading with data_rate, electric_current, voltage, information, and unitless units (#87202)
* Add missing converts to recorder/statistics_during_period API

This was resulting in the stats graphs not loading on the frontend

* its in two places
2023-02-02 16:46:16 -05:00
starkillerOG
a58e4e0f88 Reolink unsubscribe webhook when first refresh fails (#87147)
* catch ValueError on webhook async_register

* add ONVIF to webhook_id

* Unsubscribe webhook when ConfigEntryNotReady for async_config_entry_first_refresh

* Revert catching ValueError
2023-02-02 16:46:15 -05:00
Dmitry Vlasov
56a583e6ac Add missing supported features to Z-Wave.Me siren (#87141) 2023-02-02 16:46:14 -05:00
epenet
517e89ab3c Add missing converters to recorder statistics (#87137) 2023-02-02 16:46:13 -05:00
epenet
ef8029ebbf Fix invalid state class in renault (#87135) 2023-02-02 16:46:12 -05:00
starkillerOG
264b6d4f77 Bump reolink-aio to 0.3.2 (#87121) 2023-02-02 16:46:11 -05:00
starkillerOG
b24d0a86ee Bump reolink_aio to 0.3.1 (#87118) 2023-02-02 16:46:10 -05:00
shbatm
8a7e2922c2 Support ISY994 Z-Wave motorized blinds as cover (#87102) 2023-02-02 16:46:09 -05:00
Franck Nijhof
5fe3adff57 2023.2.0 (#87101) 2023-02-01 19:33:00 +01:00
Joakim Sørensen
4641497806 Bump isort from 5.11.4 to 5.12.0 (#86890) 2023-02-01 18:47:22 +01:00
Franck Nijhof
eed15bb9fa Bumped version to 2023.2.0 2023-02-01 18:41:13 +01:00
Bram Kragten
7028aa7dac Update frontend to 20230201.0 (#87099) 2023-02-01 18:40:59 +01:00
Daniel Hjelseth Høyer
6c93b28374 Update pyTibber to 0.26.12 (#87098) 2023-02-01 18:40:55 +01:00
Paulus Schoutsen
65286d0544 Fix Assist skipping entities that are hidden or have entity category (#87096)
Skipping entities that are hidden or have entity category
2023-02-01 18:40:51 +01:00
J. Nick Koston
a678eee31b Reduce chance of queue overflow during schema migration (#87090)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-02-01 18:40:47 +01:00
mkmer
fe541583a8 Bump AIOAladdinConnect to 0.1.55 (#87086) 2023-02-01 18:40:44 +01:00
mkmer
eabcfa419e Bump AIOAladdinConnect to 0.1.54 (#86749) 2023-02-01 18:40:39 +01:00
Paulus Schoutsen
d57ce25287 Bumped version to 2023.2.0b9 2023-01-31 23:16:09 -05:00
Paulus Schoutsen
c786fe27d7 Guard what version we can install ESPHome updates with (#87059)
* Guard what version we can install ESPHome updates with

* Update homeassistant/components/esphome/dashboard.py
2023-01-31 23:16:04 -05:00
Paulus Schoutsen
fd3d76988e Trigger update of ESPHome update entity when static info updates (#87058)
Trigger update of update entity when static info updates
2023-01-31 23:16:03 -05:00
J. Nick Koston
c43174ee4b Ensure humidity is still exported to HomeKit when it is read-only (#87051)
* Ensure humidity is still exported to HomeKit when is cannot be set

We would only send humidity to HomeKit if the device supported
changing the humidity

* remove unrelated changes
2023-01-31 23:16:02 -05:00
Paulus Schoutsen
0bae47c992 Bumped version to 2023.2.0b8 2023-01-31 15:16:21 -05:00
Paulus Schoutsen
0d3a368a1f Improve JSON errors from HTTP view (#87042) 2023-01-31 15:16:16 -05:00
Franck Nijhof
c7871d13cf Fix Yamaha MusicCast zone sleep select entity (#87041) 2023-01-31 15:16:15 -05:00
J. Nick Koston
3d6ced2a16 Add a repair issue when using MariaDB is affected by MDEV-25020 (#87040)
closes https://github.com/home-assistant/core/issues/83787
2023-01-31 15:16:14 -05:00
Michael Hansen
2f403b712c Bump home-assistant-intents to 2023.1.31 (#87034) 2023-01-31 15:16:12 -05:00
mkmer
1caca91174 Honeywell Correct key name (#87018)
* Correct key name

* Logic error around setpoint and auto mode

* Set tempurature and setpoints correctly

* Only high/low in auto.
2023-01-31 15:16:11 -05:00
Franck Nijhof
1859dcf99b Only report invalid numeric value for sensors once (#87010) 2023-01-31 15:16:10 -05:00
Bouwe Westerdijk
c34eb1ad9d Bump plugwise to v0.27.5 (#87001)
fixes undefined
2023-01-31 15:16:08 -05:00
Paulus Schoutsen
be69e9579c Bump ESPHome Dashboard API 1.2.3 (#86997) 2023-01-31 15:16:07 -05:00
Michael Davie
ac6fa3275b Bump env_canada to 0.5.27 (#86996)
fixes undefined
2023-01-31 15:16:06 -05:00
Paulus Schoutsen
2f896c5df8 Bumped version to 2023.2.0b7 2023-01-30 23:47:52 -05:00
Paulus Schoutsen
c9e86ccd38 ESPHome handle remove password and no encryption (#86995)
* ESPHome handle remove password and no encryption

* Start reauth for invalid api password

---------

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-01-30 23:47:48 -05:00
Paulus Schoutsen
8760227296 ESPHome discovered dashboard checks reauth flows (#86993) 2023-01-30 23:47:47 -05:00
Michael Hansen
edf02b70ea Prioritize entity names over area names in Assist matching (#86982)
* Refactor async_match_states

* Check entity name after state, before aliases

* Give entity name matches priority over area names

* Don't force result to have area

* Add area alias in tests

* Move name/area list creation back

* Clean up PR

* More clean up
2023-01-30 23:47:46 -05:00
shbatm
32a7ae6129 Bump pyisy to 3.1.11 (#86981)
* Bump pyisy to 3.1.10

* Bump pyisy to 3.1.11
2023-01-30 23:47:45 -05:00
Paulus Schoutsen
29056f1bd7 Check dashboard when showing reauth form (#86980) 2023-01-30 23:47:44 -05:00
puddly
01dea7773a Bump ZHA dependencies (#86979)
Bump ZHA dependency bellows from 0.34.6 to 0.34.7
2023-01-30 23:47:43 -05:00
Bram Kragten
688bba15ac Update frontend to 20230130.0 (#86978) 2023-01-30 23:47:42 -05:00
Franck Nijhof
f6230e2d71 Allow any state class when using the precipitation device class (#86977) 2023-01-30 23:47:41 -05:00
Steven Looman
6a9f06d36e Ensure a proper scope_id is given for IPv6 addresses when initializing the SSDP component (#86975)
fixes undefined
2023-01-30 23:47:40 -05:00
Steven Looman
dc50a6899a Fix error on empty location in ssdp messages (#86970) 2023-01-30 23:47:39 -05:00
Paul Bottein
d39d4d6b7f Uses PolledSmartEnergySummation for ZLinky (#86960) 2023-01-30 23:47:38 -05:00
ollo69
6a1710063a Catch AndroidTV exception on setup (#86819)
fixes undefined
2023-01-30 23:47:37 -05:00
puddly
565a9735fc ZHA config flow cleanup (#86742)
fixes undefined
2023-01-30 23:47:36 -05:00
mkmer
ba966bd0f7 Honeywell auto mode invalid attribute (#86728)
fixes undefined
2023-01-30 23:47:35 -05:00
Michael Hansen
c7b944ca75 Use device area id in intent matching (#86678)
* Use device area id when matching

* Normalize whitespace in response

* Add extra test entity
2023-01-30 23:47:34 -05:00
Paulus Schoutsen
0702314dcb Bumped version to 2023.2.0b6 2023-01-30 14:39:37 -05:00
Michael Hansen
81de0bba22 Performance improvements for Assist (#86966)
* Move hassil recognize into executor

* Bump hassil to 0.2.6

* Disable template parsing in name/area lists

* Don't iterate over hass.config.components directly
2023-01-30 14:38:27 -05:00
Paulus Schoutsen
0b015d46c3 Fix some mobile app sensor registration/update issues (#86965)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-01-30 14:38:26 -05:00
J. Nick Koston
0713e034b9 Silence spurious warnings about removing ix_states_entity_id with newer installs (#86961)
* Silence spurious warnings about removing ix_states_entity_id with newer installs

https://ptb.discord.com/channels/330944238910963714/427516175237382144/1069648035459641465

* Silence spurious warnings about removing ix_states_entity_id with newer installs

https://ptb.discord.com/channels/330944238910963714/427516175237382144/1069648035459641465
2023-01-30 14:38:25 -05:00
Mick Vleeshouwer
171acc22ca Fix ThreeWayHandle sensor in Overkiz integration (#86953)
Fix typo in sensor.py

Fixes https://github.com/home-assistant/core/issues/85913
2023-01-30 14:38:24 -05:00
J. Nick Koston
2e26a40bba Speed up live history setup if there is no pending data to commit (#86942) 2023-01-30 14:38:23 -05:00
Jan Bouwhuis
3f717ae854 Fix MQTT discovery failing after bad config update (#86935)
* Fix MQTT discovery failing after bad config update

* Update last discovery payload after update success

* Improve test, correct update assignment

* send_discovery_done to finally-catch vol.Error

* Just use try..finally

* Remove extra line

* use elif to avoid log confusion
2023-01-30 14:38:21 -05:00
Paulus Schoutsen
a491bfe84c Bumped version to 2023.2.0b5 2023-01-30 09:13:30 -05:00
Erik Montnemery
423acfa93b Drop minus sign on negative zero (#86939)
* Drop minus sign on negative zero

* Add tests
2023-01-30 09:13:26 -05:00
J. Nick Koston
f14771ccf2 Fix old indices not being removed in schema migration leading to slow MySQL queries (#86917)
fixes #83787
2023-01-30 09:13:25 -05:00
Thomas Schamm
07e9b0e98b Add Bosch SHC description and host form strings (#86897)
* Add description to setup SHC II. Add missing host info in reauth_confirm

* Remove template value in en.json
2023-01-30 09:13:24 -05:00
J. Nick Koston
0d27ee4fd8 Cache the names and area lists in the default agent (#86874)
* Cache the names and area lists in the default agent

fixes #86803

* add coverage to make sure the entity cache busts

* add areas test

* cover the last line
2023-01-30 09:13:22 -05:00
Robert Hillis
71b13d8f3e Address Google mail late review (#86847) 2023-01-30 09:13:21 -05:00
Tom Puttemans
63c218060b Ignore empty payloads from DSMR Reader (#86841)
* Ignore empty payloads from DSMR Reader

* Simplify empty payload handling

If the native value hasn't changed, requesting to store it won't have a performance impact.

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2023-01-30 09:13:20 -05:00
Paulus Schoutsen
8a9de2671b Bumped version to 2023.2.0b4 2023-01-28 22:07:57 -05:00
J. Nick Koston
85d5ea2eca Fix v32 schema migration when MySQL global.time_zone is configured with non-UTC timezone (#86867)
* Fix v32 schema migration when MySQL timezone is not UTC

* tweak
2023-01-28 22:07:54 -05:00
Martin Hjelmare
55b5b36c47 Fix tradfri air quality device class (#86861) 2023-01-28 22:07:52 -05:00
J. Nick Koston
c9cf3c29f8 Improve websocket throughput of state changes (#86855)
After the start event we tend to get an event storm of state
changes which can get the websocket behind. #86854 will
help with that a bit, but we can reduce the overhead
to build a state diff when the attributes have not
changed
2023-01-28 22:07:52 -05:00
Robert Hillis
6db9653a87 Fix D-Link attributes (#86842)
* Fix D-Link attributes

* fix blocking call
2023-01-28 22:07:51 -05:00
Paul Bottein
9adaf27064 Update frontend to 20230128.0 (#86838) 2023-01-28 22:07:50 -05:00
Paulus Schoutsen
69ed30f743 Bumped version to 2023.2.0b3 2023-01-27 22:54:05 -05:00
shbatm
d33373f6ee Check for missing ISY994 Z-Wave Properties (#86829)
* Check for missing Z-Wave Properties

* Fix black from mobile
2023-01-27 22:54:01 -05:00
Robert Hillis
bedf5fe6cd Fix D-Link config flow auth (#86824) 2023-01-27 22:54:00 -05:00
Bouwe Westerdijk
29eb7e8f9e Bump plugwise to v0.27.4 (#86812)
fixes undefined
2023-01-27 22:53:59 -05:00
J. Nick Koston
60b96f19b7 Fix Bluetooth discoveries missing between restarts (#86808)
* Fix Bluetooth discoveries missing between restarts

* do not load other integrations

* coverage
2023-01-27 22:53:58 -05:00
J. Nick Koston
0a6ce35e30 Chunk MariaDB and Postgresql data migration to avoid running out of buffer space (#86680)
* Chunk MariaDB data migration to avoid running out of buffer space

This will make the migration slower but since the innodb_buffer_pool_size
is using the defaul to 128M and not tuned to the db size there is a
risk of running out of buffer space for large databases

* Update homeassistant/components/recorder/migration.py

* hard code since bandit thinks its an injection

* Update homeassistant/components/recorder/migration.py

* guard against manually modified data/corrupt db

* adjust to 10k per chunk

* adjust to 50k per chunk

* memory still just fine at 250k

* but slower

* commit after each chunk to reduce lock pressure

* adjust

* set to 0 if null so we do not loop forever (this should only happen if the data is missing)

* set to 0 if null so we do not loop forever (this should only happen if the data is missing)

* tweak

* tweak

* limit cleanup

* lower limit to give some more buffer

* lower limit to give some more buffer

* where required for sqlite

* sqlite can wipe as many as needed with no limit

* limit on mysql only

* chunk postgres

* fix limit

* tweak

* fix reference

* fix

* tweak for ram

* postgres memory reduction

* defer cleanup

* fix

* same order
2023-01-27 22:53:57 -05:00
Paulus Schoutsen
6397cc5d04 Bumped version to 2023.2.0b2 2023-01-26 21:47:21 -05:00
Jesse Hills
b7311dc655 Remove esphome password from config flow data if not needed (#86763)
* Remove esphome password if not needed

* Add test

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-01-26 21:47:00 -05:00
Paulus Schoutsen
e20c7491c1 ESPHome update: Store reference to runtime data, not one of its values (#86762)
Store reference to runtime data, not one of its values
2023-01-26 21:46:59 -05:00
Aaron Bach
8cbefd5f97 Fix state class issues in Ambient PWS (#86758)
fixes undefined
2023-01-26 21:46:58 -05:00
Paulus Schoutsen
c7665b479a OpenAI: Fix device without model (#86754) 2023-01-26 21:46:57 -05:00
Shay Levy
4f2966674a Bump aioshelly to 5.3.1 (#86751) 2023-01-26 21:46:56 -05:00
Franck Nijhof
b464179eac Fix state classes for duration device class (#86727) 2023-01-26 21:46:55 -05:00
Franck Nijhof
cd59705c4b Remove gas device class from current sensor in dsmr_reader (#86725) 2023-01-26 21:46:54 -05:00
Martin Hjelmare
77bd23899f Bump python-matter-server to 2.0.2 (#86712) 2023-01-26 21:46:53 -05:00
David F. Mulcahey
d211603ba7 Update Inovelli Blue Series switch support in ZHA (#86711) 2023-01-26 21:46:52 -05:00
Erik Montnemery
1dc3bb6eb1 Terminate strings at NUL when recording states and events (#86687) 2023-01-26 21:46:51 -05:00
Robert Svensson
22afc7c7fb Fix missing interface key in deCONZ logbook (#86684)
fixes undefined
2023-01-26 21:46:50 -05:00
Paulus Schoutsen
ba82f13821 Make openai conversation prompt template more readable + test case (#86676) 2023-01-26 21:46:49 -05:00
MHFDoge
41add96bab Add known webostv button to list (#86674)
Add known button to list.
2023-01-26 21:46:48 -05:00
Andrey Kupreychik
c8c3f4bef6 Update ndms2_client to 0.1.2 (#86624)
fix https://github.com/home-assistant/core/issues/86379
fixes undefined
2023-01-26 21:46:47 -05:00
Patrick ZAJDA
8cb8ecdae9 Migrate Nuki to new entity naming style (#80021)
Co-authored-by: Pascal Vizeli <pvizeli@syshack.ch>
2023-01-26 21:46:46 -05:00
Pascal Reeb
bd1371680f Add device registration to the Nuki component (#79806)
* Add device registration to the Nuki component

* Name is always given by the API

* implement pvizeli's suggestions

* switch device_registry to snake_case

* fix entity naming

* unify manufacturer names
2023-01-26 21:46:45 -05:00
Paulus Schoutsen
8f684e962a Bumped version to 2023.2.0b1 2023-01-25 23:00:35 -05:00
Paulus Schoutsen
07a1259db9 Add error handling for OpenAI (#86671)
* Add error handling for OpenAI

* Simplify area filtering

* better prompt
2023-01-25 23:00:28 -05:00
David F. Mulcahey
ea2bf34647 Bump ZHA quirks lib (#86669) 2023-01-25 23:00:27 -05:00
J. Nick Koston
a6fdf1d09a Correct units on mopeka battery voltage sensor (#86663) 2023-01-25 23:00:26 -05:00
Paulus Schoutsen
9ca04dbfa1 Google Assistant: unset agent on unload (#86635) 2023-01-25 23:00:24 -05:00
Paulus Schoutsen
e1c8dff536 Fix oauth2 error (#86634) 2023-01-25 23:00:23 -05:00
Joakim Plate
a1416b9044 Print expected device class units in error log (#86125) 2023-01-25 23:00:22 -05:00
Franck Nijhof
123aafd772 Bumped version to 2023.2.0b0 2023-01-25 18:39:20 +01:00
150 changed files with 2759 additions and 708 deletions

View File

@@ -60,7 +60,7 @@ repos:
- --configfile=tests/bandit.yaml
files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/PyCQA/isort
rev: 5.11.4
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks

View File

@@ -2,7 +2,7 @@
"domain": "aladdin_connect",
"name": "Aladdin Connect",
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"requirements": ["AIOAladdinConnect==0.1.53"],
"requirements": ["AIOAladdinConnect==0.1.55"],
"codeowners": ["@mkmer"],
"iot_class": "cloud_polling",
"loggers": ["aladdin_connect"],

View File

@@ -128,7 +128,6 @@ SENSOR_DESCRIPTIONS = (
key=TYPE_AQI_PM25_24H,
name="AQI PM2.5 24h avg",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key=TYPE_AQI_PM25_IN,
@@ -140,7 +139,6 @@ SENSOR_DESCRIPTIONS = (
key=TYPE_AQI_PM25_IN_24H,
name="AQI PM2.5 indoor 24h avg",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key=TYPE_BAROMABSIN,
@@ -182,7 +180,7 @@ SENSOR_DESCRIPTIONS = (
name="Event rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key=TYPE_FEELSLIKE,
@@ -287,7 +285,6 @@ SENSOR_DESCRIPTIONS = (
name="Last rain",
icon="mdi:water",
device_class=SensorDeviceClass.TIMESTAMP,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_LIGHTNING_PER_DAY,
@@ -315,7 +312,7 @@ SENSOR_DESCRIPTIONS = (
name="Monthly rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key=TYPE_PM25_24H,
@@ -586,7 +583,7 @@ SENSOR_DESCRIPTIONS = (
name="Lifetime rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key=TYPE_UV,
@@ -599,7 +596,7 @@ SENSOR_DESCRIPTIONS = (
name="Weekly rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key=TYPE_WINDDIR,

View File

@@ -6,6 +6,13 @@ import os
from typing import Any
from adb_shell.auth.keygen import keygen
from adb_shell.exceptions import (
AdbTimeoutError,
InvalidChecksumError,
InvalidCommandError,
InvalidResponseError,
TcpTimeoutException,
)
from androidtv.adb_manager.adb_manager_sync import ADBPythonSync, PythonRSASigner
from androidtv.setup_async import (
AndroidTVAsync,
@@ -43,6 +50,18 @@ from .const import (
SIGNAL_CONFIG_ENTITY,
)
ADB_PYTHON_EXCEPTIONS: tuple = (
AdbTimeoutError,
BrokenPipeError,
ConnectionResetError,
ValueError,
InvalidChecksumError,
InvalidCommandError,
InvalidResponseError,
TcpTimeoutException,
)
ADB_TCP_EXCEPTIONS: tuple = (ConnectionResetError, RuntimeError)
PLATFORMS = [Platform.MEDIA_PLAYER]
RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
@@ -132,9 +151,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Android TV platform."""
state_det_rules = entry.options.get(CONF_STATE_DETECTION_RULES)
aftv, error_message = await async_connect_androidtv(
hass, entry.data, state_detection_rules=state_det_rules
)
if CONF_ADB_SERVER_IP not in entry.data:
exceptions = ADB_PYTHON_EXCEPTIONS
else:
exceptions = ADB_TCP_EXCEPTIONS
try:
aftv, error_message = await async_connect_androidtv(
hass, entry.data, state_detection_rules=state_det_rules
)
except exceptions as exc:
raise ConfigEntryNotReady(exc) from exc
if not aftv:
raise ConfigEntryNotReady(error_message)

View File

@@ -7,13 +7,6 @@ import functools
import logging
from typing import Any, Concatenate, ParamSpec, TypeVar
from adb_shell.exceptions import (
AdbTimeoutError,
InvalidChecksumError,
InvalidCommandError,
InvalidResponseError,
TcpTimeoutException,
)
from androidtv.constants import APPS, KEYS
from androidtv.exceptions import LockNotAcquiredException
import voluptuous as vol
@@ -42,7 +35,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import get_androidtv_mac
from . import ADB_PYTHON_EXCEPTIONS, ADB_TCP_EXCEPTIONS, get_androidtv_mac
from .const import (
ANDROID_DEV,
ANDROID_DEV_OPT,
@@ -252,19 +245,10 @@ class ADBDevice(MediaPlayerEntity):
# ADB exceptions to catch
if not aftv.adb_server_ip:
# Using "adb_shell" (Python ADB implementation)
self.exceptions = (
AdbTimeoutError,
BrokenPipeError,
ConnectionResetError,
ValueError,
InvalidChecksumError,
InvalidCommandError,
InvalidResponseError,
TcpTimeoutException,
)
self.exceptions = ADB_PYTHON_EXCEPTIONS
else:
# Using "pure-python-adb" (communicate with ADB server)
self.exceptions = (ConnectionResetError, RuntimeError)
self.exceptions = ADB_TCP_EXCEPTIONS
# Property attributes
self._attr_extra_state_attributes = {

View File

@@ -209,6 +209,20 @@ class BluetoothManager:
self._bluetooth_adapters, self.storage
)
self.async_setup_unavailable_tracking()
seen: set[str] = set()
for address, service_info in itertools.chain(
self._connectable_history.items(), self._all_history.items()
):
if address in seen:
continue
seen.add(address)
for domain in self._integration_matcher.match_domains(service_info):
discovery_flow.async_create_flow(
self.hass,
domain,
{"source": config_entries.SOURCE_BLUETOOTH},
service_info,
)
@hass_callback
def async_stop(self, event: Event) -> None:

View File

@@ -14,11 +14,14 @@
}
},
"confirm_discovery": {
"description": "Please press the Bosch Smart Home Controller's front-side button until LED starts flashing.\nReady to continue to set up {model} @ {host} with Home Assistant?"
"description": "Smart Home Controller I: Please press the front-side button until LED starts flashing.\nSmart Home Controller II: Press the function button shortly. Cloud and network lights start blinking orange.\nDevice is now ready to be paired.\n\nReady to continue to set up {model} @ {host} with Home Assistant?"
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The bosch_shc integration needs to re-authenticate your account"
"description": "The bosch_shc integration needs to re-authenticate your account",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
}
},
"error": {

View File

@@ -14,7 +14,7 @@
"flow_title": "Bosch SHC: {name}",
"step": {
"confirm_discovery": {
"description": "Bitte dr\u00fccke die frontseitige Taste des Bosch Smart Home Controllers, bis die LED zu blinken beginnt.\nBist du bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?"
"description": "Smart Home Controller I: Bitte dr\u00fccke die frontseitige Taste, bis die LED zu blinken beginnt.\nSmart Home Controller II: Dr\u00fccke kurz die Funktionstaste. Die Cloud- und Netzwerkleuchten beginnen orange zu blinken.\nDas Ger\u00e4t ist nun f\u00fcr die Kopplung bereit.\n\nBist du bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?"
},
"credentials": {
"data": {
@@ -23,7 +23,10 @@
},
"reauth_confirm": {
"description": "Die bosch_shc Integration muss dein Konto neu authentifizieren",
"title": "Integration erneut authentifizieren"
"title": "Integration erneut authentifizieren",
"data": {
"host": "Host"
}
},
"user": {
"data": {

View File

@@ -14,7 +14,7 @@
"flow_title": "Bosch SHC: {name}",
"step": {
"confirm_discovery": {
"description": "Please press the Bosch Smart Home Controller's front-side button until LED starts flashing.\nReady to continue to set up {model} @ {host} with Home Assistant?"
"description": "Smart Home Controller I: Please press the front-side button until LED starts flashing.\nSmart Home Controller II: Press the function button shortly. Cloud and network lights start blinking orange.\nDevice is now ready to be paired.\n\nReady to continue to set up {model} @ {host} with Home Assistant?"
},
"credentials": {
"data": {
@@ -23,7 +23,10 @@
},
"reauth_confirm": {
"description": "The bosch_shc integration needs to re-authenticate your account",
"title": "Reauthenticate Integration"
"title": "Reauthenticate Integration",
"data": {
"host": "Host"
}
},
"user": {
"data": {

View File

@@ -11,7 +11,7 @@ import re
from typing import IO, Any
from hassil.intents import Intents, ResponseType, SlotList, TextSlotList
from hassil.recognize import recognize
from hassil.recognize import RecognizeResult, recognize_all
from hassil.util import merge_dict
from home_assistant_intents import get_intents
import yaml
@@ -71,6 +71,8 @@ class DefaultAgent(AbstractConversationAgent):
# intent -> [sentences]
self._config_intents: dict[str, Any] = {}
self._areas_list: TextSlotList | None = None
self._names_list: TextSlotList | None = None
async def async_initialize(self, config_intents):
"""Initialize the default agent."""
@@ -81,6 +83,22 @@ class DefaultAgent(AbstractConversationAgent):
if config_intents:
self._config_intents = config_intents
self.hass.bus.async_listen(
area_registry.EVENT_AREA_REGISTRY_UPDATED,
self._async_handle_area_registry_changed,
run_immediately=True,
)
self.hass.bus.async_listen(
entity_registry.EVENT_ENTITY_REGISTRY_UPDATED,
self._async_handle_entity_registry_changed,
run_immediately=True,
)
self.hass.bus.async_listen(
core.EVENT_STATE_CHANGED,
self._async_handle_state_changed,
run_immediately=True,
)
async def async_process(self, user_input: ConversationInput) -> ConversationResult:
"""Process a sentence."""
language = user_input.language or self.hass.config.language
@@ -109,7 +127,12 @@ class DefaultAgent(AbstractConversationAgent):
"name": self._make_names_list(),
}
result = recognize(user_input.text, lang_intents.intents, slot_lists=slot_lists)
result = await self.hass.async_add_executor_job(
self._recognize,
user_input,
lang_intents,
slot_lists,
)
if result is None:
_LOGGER.debug("No intent was matched for '%s'", user_input.text)
return _make_error_result(
@@ -160,21 +183,43 @@ class DefaultAgent(AbstractConversationAgent):
).get(response_key)
if response_str:
response_template = template.Template(response_str, self.hass)
intent_response.async_set_speech(
response_template.async_render(
{
"slots": {
entity_name: entity_value.text or entity_value.value
for entity_name, entity_value in result.entities.items()
}
speech = response_template.async_render(
{
"slots": {
entity_name: entity_value.text or entity_value.value
for entity_name, entity_value in result.entities.items()
}
)
}
)
# Normalize whitespace
speech = " ".join(speech.strip().split())
intent_response.async_set_speech(speech)
return ConversationResult(
response=intent_response, conversation_id=conversation_id
)
def _recognize(
self,
user_input: ConversationInput,
lang_intents: LanguageIntents,
slot_lists: dict[str, SlotList],
) -> RecognizeResult | None:
"""Search intents for a match to user input."""
# Prioritize matches with entity names above area names
maybe_result: RecognizeResult | None = None
for result in recognize_all(
user_input.text, lang_intents.intents, slot_lists=slot_lists
):
if "name" in result.entities:
return result
# Keep looking in case an entity has the same name
maybe_result = result
return maybe_result
async def async_reload(self, language: str | None = None):
"""Clear cached intents for a language."""
if language is None:
@@ -196,13 +241,15 @@ class DefaultAgent(AbstractConversationAgent):
async def async_get_or_load_intents(self, language: str) -> LanguageIntents | None:
"""Load all intents of a language with lock."""
hass_components = set(self.hass.config.components)
async with self._lang_lock[language]:
return await self.hass.async_add_executor_job(
self._get_or_load_intents,
language,
self._get_or_load_intents, language, hass_components
)
def _get_or_load_intents(self, language: str) -> LanguageIntents | None:
def _get_or_load_intents(
self, language: str, hass_components: set[str]
) -> LanguageIntents | None:
"""Load all intents for language (run inside executor)."""
lang_intents = self._lang_intents.get(language)
@@ -215,7 +262,7 @@ class DefaultAgent(AbstractConversationAgent):
# Check if any new components have been loaded
intents_changed = False
for component in self.hass.config.components:
for component in hass_components:
if component in loaded_components:
continue
@@ -310,8 +357,29 @@ class DefaultAgent(AbstractConversationAgent):
return lang_intents
@core.callback
def _async_handle_area_registry_changed(self, event: core.Event) -> None:
"""Clear area area cache when the area registry has changed."""
self._areas_list = None
@core.callback
def _async_handle_entity_registry_changed(self, event: core.Event) -> None:
"""Clear names list cache when an entity changes aliases."""
if event.data["action"] == "update" and "aliases" not in event.data["changes"]:
return
self._names_list = None
@core.callback
def _async_handle_state_changed(self, event: core.Event) -> None:
"""Clear names list cache when a state is added or removed from the state machine."""
if event.data.get("old_state") and event.data.get("new_state"):
return
self._names_list = None
def _make_areas_list(self) -> TextSlotList:
"""Create slot list mapping area names/aliases to area ids."""
if self._areas_list is not None:
return self._areas_list
registry = area_registry.async_get(self.hass)
areas = []
for entry in registry.async_list_areas():
@@ -320,31 +388,38 @@ class DefaultAgent(AbstractConversationAgent):
for alias in entry.aliases:
areas.append((alias, entry.id))
return TextSlotList.from_tuples(areas)
self._areas_list = TextSlotList.from_tuples(areas, allow_template=False)
return self._areas_list
def _make_names_list(self) -> TextSlotList:
"""Create slot list mapping entity names/aliases to entity ids."""
if self._names_list is not None:
return self._names_list
states = self.hass.states.async_all()
registry = entity_registry.async_get(self.hass)
entities = entity_registry.async_get(self.hass)
names = []
for state in states:
domain = state.entity_id.split(".", maxsplit=1)[0]
context = {"domain": domain}
context = {"domain": state.domain}
entry = registry.async_get(state.entity_id)
if entry is not None:
if entry.entity_category:
# Skip configuration/diagnostic entities
entity = entities.async_get(state.entity_id)
if entity is not None:
if entity.entity_category or entity.hidden:
# Skip configuration/diagnostic/hidden entities
continue
if entry.aliases:
for alias in entry.aliases:
if entity.aliases:
for alias in entity.aliases:
names.append((alias, state.entity_id, context))
# Default name
names.append((state.name, state.entity_id, context))
# Default name
names.append((state.name, state.entity_id, context))
return TextSlotList.from_tuples(names)
else:
# Default name
names.append((state.name, state.entity_id, context))
self._names_list = TextSlotList.from_tuples(names, allow_template=False)
return self._names_list
def _get_error_text(
self, response_type: ResponseType, lang_intents: LanguageIntents

View File

@@ -2,7 +2,7 @@
"domain": "conversation",
"name": "Conversation",
"documentation": "https://www.home-assistant.io/integrations/conversation",
"requirements": ["hassil==0.2.5", "home-assistant-intents==2023.1.25"],
"requirements": ["hassil==0.2.6", "home-assistant-intents==2023.1.31"],
"dependencies": ["http"],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal",

View File

@@ -17,6 +17,10 @@ from .device_trigger import (
CONF_BUTTON_2,
CONF_BUTTON_3,
CONF_BUTTON_4,
CONF_BUTTON_5,
CONF_BUTTON_6,
CONF_BUTTON_7,
CONF_BUTTON_8,
CONF_CLOSE,
CONF_DIM_DOWN,
CONF_DIM_UP,
@@ -95,6 +99,10 @@ INTERFACES = {
CONF_BUTTON_2: "Button 2",
CONF_BUTTON_3: "Button 3",
CONF_BUTTON_4: "Button 4",
CONF_BUTTON_5: "Button 5",
CONF_BUTTON_6: "Button 6",
CONF_BUTTON_7: "Button 7",
CONF_BUTTON_8: "Button 8",
CONF_SIDE_1: "Side 1",
CONF_SIDE_2: "Side 2",
CONF_SIDE_3: "Side 3",

View File

@@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_USERNAME],
entry.data[CONF_USE_LEGACY_PROTOCOL],
)
if not smartplug.authenticated and entry.data[CONF_USE_LEGACY_PROTOCOL]:
if not smartplug.authenticated and smartplug.use_legacy_protocol:
raise ConfigEntryNotReady("Cannot connect/authenticate")
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SmartPlugData(smartplug)

View File

@@ -131,6 +131,6 @@ class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
except Exception as ex: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception: %s", ex)
return "unknown"
if smartplug.authenticated:
return None
return "cannot_connect"
if not smartplug.authenticated and smartplug.use_legacy_protocol:
return "cannot_connect"
return None

View File

@@ -19,9 +19,9 @@ class SmartPlugData:
"""Initialize the data object."""
self.smartplug = smartplug
self.state: str | None = None
self.temperature: str | None = None
self.current_consumption = None
self.total_consumption: str | None = None
self.temperature: str = ""
self.current_consumption: str = ""
self.total_consumption: str = ""
self.available = False
self._n_tried = 0
self._last_tried: datetime | None = None

View File

@@ -94,17 +94,22 @@ class SmartPlugSwitch(DLinkEntity, SwitchEntity):
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the device."""
attrs: dict[str, Any] = {}
if self.data.temperature and self.data.temperature.isnumeric():
attrs[ATTR_TEMPERATURE] = self.hass.config.units.temperature(
try:
temperature = self.hass.config.units.temperature(
int(self.data.temperature), UnitOfTemperature.CELSIUS
)
else:
attrs[ATTR_TEMPERATURE] = None
if self.data.total_consumption and self.data.total_consumption.isnumeric():
attrs[ATTR_TOTAL_CONSUMPTION] = float(self.data.total_consumption)
else:
attrs[ATTR_TOTAL_CONSUMPTION] = None
except ValueError:
temperature = None
try:
total_consumption = float(self.data.total_consumption)
except ValueError:
total_consumption = None
attrs = {
ATTR_TOTAL_CONSUMPTION: total_consumption,
ATTR_TEMPERATURE: temperature,
}
return attrs

View File

@@ -3,7 +3,7 @@
"name": "DLNA Digital Media Renderer",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"requirements": ["async-upnp-client==0.33.0", "getmac==0.8.2"],
"requirements": ["async-upnp-client==0.33.1", "getmac==0.8.2"],
"dependencies": ["ssdp"],
"after_dependencies": ["media_source"],
"ssdp": [

View File

@@ -3,7 +3,7 @@
"name": "DLNA Digital Media Server",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
"requirements": ["async-upnp-client==0.33.0"],
"requirements": ["async-upnp-client==0.33.1"],
"dependencies": ["ssdp"],
"after_dependencies": ["media_source"],
"ssdp": [

View File

@@ -209,7 +209,6 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
DSMRReaderSensorEntityDescription(
key="dsmr/consumption/gas/currently_delivered",
name="Current gas usage",
device_class=SensorDeviceClass.GAS,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
state_class=SensorStateClass.MEASUREMENT,
),

View File

@@ -69,7 +69,10 @@ class DSMRSensor(SensorEntity):
@callback
def message_received(message):
"""Handle new MQTT messages."""
if self.entity_description.state is not None:
if message.payload == "":
self._attr_native_value = None
elif self.entity_description.state is not None:
# Perform optional additional parsing
self._attr_native_value = self.entity_description.state(message.payload)
else:
self._attr_native_value = message.payload

View File

@@ -2,7 +2,7 @@
"domain": "environment_canada",
"name": "Environment Canada",
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"requirements": ["env_canada==0.5.22"],
"requirements": ["env_canada==0.5.27"],
"codeowners": ["@gwww", "@michaeldavie"],
"config_flow": true,
"iot_class": "cloud_polling",

View File

@@ -17,6 +17,7 @@ from aioesphomeapi import (
EntityInfo,
EntityState,
HomeassistantServiceCall,
InvalidAuthAPIError,
InvalidEncryptionKeyAPIError,
ReconnectLogic,
RequiresEncryptionAPIError,
@@ -347,7 +348,14 @@ async def async_setup_entry( # noqa: C901
async def on_connect_error(err: Exception) -> None:
"""Start reauth flow if appropriate connect error type."""
if isinstance(err, (RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError)):
if isinstance(
err,
(
RequiresEncryptionAPIError,
InvalidEncryptionKeyAPIError,
InvalidAuthAPIError,
),
):
entry.async_start_reauth(hass)
reconnect_logic = ReconnectLogic(
@@ -638,9 +646,10 @@ async def platform_async_setup_entry(
# Add entities to Home Assistant
async_add_entities(add_entities)
signal = f"esphome_{entry.entry_id}_on_list"
entry_data.cleanup_callbacks.append(
async_dispatcher_connect(hass, signal, async_list_entities)
async_dispatcher_connect(
hass, entry_data.signal_static_info_updated, async_list_entities
)
)

View File

@@ -92,10 +92,18 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
self._name = entry.title
self._device_name = entry.data.get(CONF_DEVICE_NAME)
if await self._retrieve_encryption_key_from_dashboard():
error = await self.fetch_device_info()
if error is None:
return await self._async_authenticate_or_add()
# Device without encryption allows fetching device info. We can then check
# if the device is no longer using a password. If we did try with a password,
# we know setting password to empty will allow us to authenticate.
error = await self.fetch_device_info()
if (
error is None
and self._password
and self._device_info
and not self._device_info.uses_password
):
self._password = ""
return await self._async_authenticate_or_add()
return await self.async_step_reauth_confirm()
@@ -105,6 +113,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle reauthorization flow."""
errors = {}
if await self._retrieve_encryption_key_from_dashboard():
error = await self.fetch_device_info()
if error is None:
return await self._async_authenticate_or_add()
if user_input is not None:
self._noise_psk = user_input[CONF_NOISE_PSK]
error = await self.fetch_device_info()
@@ -153,6 +166,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
if self._device_info.uses_password:
return await self.async_step_authenticate()
self._password = ""
return self._async_get_entry()
async def async_step_discovery_confirm(

View File

@@ -6,9 +6,10 @@ from datetime import timedelta
import logging
import aiohttp
from awesomeversion import AwesomeVersion
from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI
from homeassistant.config_entries import ConfigEntryState
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -52,8 +53,14 @@ async def async_set_dashboard_info(
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.state == ConfigEntryState.LOADED
]
if reloads:
await asyncio.gather(*reloads)
# Re-auth flows will check the dashboard for encryption key when the form is requested
reauths = [
hass.config_entries.flow.async_configure(flow["flow_id"])
for flow in hass.config_entries.flow.async_progress()
if flow["handler"] == DOMAIN and flow["context"]["source"] == SOURCE_REAUTH
]
if reloads or reauths:
await asyncio.gather(*reloads, *reauths)
class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]):
@@ -77,6 +84,20 @@ class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]):
self.url = url
self.api = ESPHomeDashboardAPI(url, session)
@property
def supports_update(self) -> bool:
"""Return whether the dashboard supports updates."""
if self.data is None:
raise RuntimeError("Data needs to be loaded first")
if len(self.data) == 0:
return False
esphome_version: str = next(iter(self.data.values()))["current_version"]
# There is no January release
return AwesomeVersion(esphome_version) > AwesomeVersion("2023.1.0")
async def _async_update_data(self) -> dict:
"""Fetch device data."""
devices = await self.api.get_devices()

View File

@@ -107,6 +107,11 @@ class RuntimeEntryData:
return self.device_info.friendly_name
return self.name
@property
def signal_static_info_updated(self) -> str:
"""Return the signal to listen to for updates on static info."""
return f"esphome_{self.entry_id}_on_list"
@callback
def async_update_ble_connection_limits(self, free: int, limit: int) -> None:
"""Update the BLE connection limits."""
@@ -168,8 +173,7 @@ class RuntimeEntryData:
await self._ensure_platforms_loaded(hass, entry, needed_platforms)
# Then send dispatcher event
signal = f"esphome_{self.entry_id}_on_list"
async_dispatcher_send(hass, signal, infos)
async_dispatcher_send(hass, self.signal_static_info_updated, infos)
@callback
def async_subscribe_state_update(

View File

@@ -3,7 +3,7 @@
"name": "ESPHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/esphome",
"requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.2.1"],
"requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.2.3"],
"zeroconf": ["_esphomelib._tcp.local."],
"dhcp": [{ "registered_devices": true }],
"codeowners": ["@OttoWinter", "@jesserockz"],

View File

@@ -5,7 +5,7 @@ import asyncio
import logging
from typing import Any, cast
from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo
from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo, EntityInfo
from homeassistant.components.update import (
UpdateDeviceClass,
@@ -13,7 +13,7 @@ from homeassistant.components.update import (
UpdateEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
@@ -68,25 +68,30 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity):
_attr_has_entity_name = True
_attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_supported_features = UpdateEntityFeature.INSTALL
_attr_title = "ESPHome"
_attr_name = "Firmware"
_device_info: ESPHomeDeviceInfo
def __init__(
self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboard
) -> None:
"""Initialize the update entity."""
super().__init__(coordinator=coordinator)
assert entry_data.device_info is not None
self._device_info = entry_data.device_info
self._entry_data = entry_data
self._attr_unique_id = entry_data.device_info.mac_address
self._attr_device_info = DeviceInfo(
connections={
(dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address)
}
)
if coordinator.supports_update:
self._attr_supported_features = UpdateEntityFeature.INSTALL
@property
def _device_info(self) -> ESPHomeDeviceInfo:
"""Return the device info."""
assert self._entry_data.device_info is not None
return self._entry_data.device_info
@property
def available(self) -> bool:
@@ -111,6 +116,23 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity):
"""URL to the full release notes of the latest version available."""
return "https://esphome.io/changelog/"
async def async_added_to_hass(self) -> None:
"""Handle entity added to Home Assistant."""
await super().async_added_to_hass()
@callback
def _static_info_updated(infos: list[EntityInfo]) -> None:
"""Handle static info update."""
self.async_write_ha_state()
self.async_on_remove(
async_dispatcher_connect(
self.hass,
self._entry_data.signal_static_info_updated,
_static_info_updated,
)
)
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:

View File

@@ -2,7 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20230125.0"],
"requirements": ["home-assistant-frontend==20230202.0"],
"dependencies": [
"api",
"auth",

View File

@@ -98,6 +98,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
for service_name in hass.services.async_services()[DOMAIN]:
hass.services.async_remove(DOMAIN, service_name)
if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False):
conversation.async_unset_agent(hass, entry)
return True

View File

@@ -13,14 +13,22 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
)
from homeassistant.helpers.typing import ConfigType
from .api import AsyncConfigEntryAuth
from .const import DATA_AUTH, DOMAIN
from .const import DATA_AUTH, DATA_HASS_CONFIG, DOMAIN
from .services import async_setup_services
PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Google Mail platform."""
hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Google Mail from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)
@@ -36,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise ConfigEntryNotReady from err
except ClientError as err:
raise ConfigEntryNotReady from err
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = auth
hass.data[DOMAIN][entry.entry_id] = auth
hass.async_create_task(
discovery.async_load_platform(
@@ -44,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Platform.NOTIFY,
DOMAIN,
{DATA_AUTH: auth, CONF_NAME: entry.title},
{},
hass.data[DOMAIN][DATA_HASS_CONFIG],
)
)

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from typing import Any, cast
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
@@ -57,23 +57,29 @@ class OAuth2FlowHandler(
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
"""Create an entry for the flow, or update existing entry."""
if self.reauth_entry:
self.hass.config_entries.async_update_entry(self.reauth_entry, data=data)
await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")
credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
def _get_profile() -> dict[str, Any]:
def _get_profile() -> str:
"""Get profile from inside the executor."""
users = build( # pylint: disable=no-member
"gmail", "v1", credentials=credentials
).users()
return users.getProfile(userId="me").execute()
return users.getProfile(userId="me").execute()["emailAddress"]
email = (await self.hass.async_add_executor_job(_get_profile))["emailAddress"]
credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
email = await self.hass.async_add_executor_job(_get_profile)
await self.async_set_unique_id(email)
self._abort_if_unique_id_configured()
if not self.reauth_entry:
await self.async_set_unique_id(email)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=email, data=data)
return self.async_create_entry(title=email, data=data)
if self.reauth_entry.unique_id == email:
self.hass.config_entries.async_update_entry(self.reauth_entry, data=data)
await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_abort(
reason="wrong_account",
description_placeholders={"email": cast(str, self.reauth_entry.unique_id)},
)

View File

@@ -16,6 +16,7 @@ ATTR_START = "start"
ATTR_TITLE = "title"
DATA_AUTH = "auth"
DATA_HASS_CONFIG = "hass_config"
DEFAULT_ACCESS = [
"https://www.googleapis.com/auth/gmail.compose",
"https://www.googleapis.com/auth/gmail.settings.basic",

View File

@@ -1,6 +1,7 @@
"""Entity representing a Google Mail account."""
from __future__ import annotations
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
from .api import AsyncConfigEntryAuth
@@ -24,6 +25,7 @@ class GoogleMailEntity(Entity):
f"{auth.oauth_session.config_entry.entry_id}_{description.key}"
)
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, auth.oauth_session.config_entry.entry_id)},
manufacturer=MANUFACTURER,
name=auth.oauth_session.config_entry.unique_id,

View File

@@ -7,5 +7,5 @@
"requirements": ["google-api-python-client==2.71.0"],
"codeowners": ["@tkdrob"],
"iot_class": "cloud_polling",
"integration_type": "device"
"integration_type": "service"
}

View File

@@ -3,10 +3,9 @@ from __future__ import annotations
import base64
from email.message import EmailMessage
from typing import Any, cast
from typing import Any
from googleapiclient.http import HttpRequest
import voluptuous as vol
from homeassistant.components.notify import (
ATTR_DATA,
@@ -27,9 +26,9 @@ async def async_get_service(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> GMailNotificationService:
) -> GMailNotificationService | None:
"""Get the notification service."""
return GMailNotificationService(cast(DiscoveryInfoType, discovery_info))
return GMailNotificationService(discovery_info) if discovery_info else None
class GMailNotificationService(BaseNotificationService):
@@ -61,6 +60,6 @@ class GMailNotificationService(BaseNotificationService):
msg = users.drafts().create(userId=email["From"], body={ATTR_MESSAGE: body})
else:
if not to_addrs:
raise vol.Invalid("recipient address required")
raise ValueError("recipient address required")
msg = users.messages().send(userId=email["From"], body=body)
await self.hass.async_add_executor_job(msg.execute)

View File

@@ -21,7 +21,8 @@
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
"unknown": "[%key:common::config_flow::error::unknown%]",
"wrong_account": "Wrong account: Please authenticate with {email}."
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@@ -12,7 +12,8 @@
"oauth_error": "Received invalid token data.",
"reauth_successful": "Re-authentication was successful",
"timeout_connect": "Timeout establishing connection",
"unknown": "Unexpected error"
"unknown": "Unexpected error",
"wrong_account": "Wrong account: Please authenticate with {email}."
},
"create_entry": {
"default": "Successfully authenticated"

View File

@@ -188,8 +188,14 @@ class Thermostat(HomeAccessory):
(CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
)
if (
ATTR_CURRENT_HUMIDITY in attributes
or features & ClimateEntityFeature.TARGET_HUMIDITY
):
self.chars.append(CHAR_CURRENT_HUMIDITY)
if features & ClimateEntityFeature.TARGET_HUMIDITY:
self.chars.extend((CHAR_TARGET_HUMIDITY, CHAR_CURRENT_HUMIDITY))
self.chars.append(CHAR_TARGET_HUMIDITY)
serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars)
self.set_primary_service(serv_thermostat)
@@ -253,7 +259,6 @@ class Thermostat(HomeAccessory):
properties={PROP_MIN_VALUE: hc_min_temp, PROP_MAX_VALUE: hc_max_temp},
)
self.char_target_humidity = None
self.char_current_humidity = None
if CHAR_TARGET_HUMIDITY in self.chars:
self.char_target_humidity = serv_thermostat.configure_char(
CHAR_TARGET_HUMIDITY,
@@ -265,6 +270,8 @@ class Thermostat(HomeAccessory):
# of 0-80%
properties={PROP_MIN_VALUE: min_humidity},
)
self.char_current_humidity = None
if CHAR_CURRENT_HUMIDITY in self.chars:
self.char_current_humidity = serv_thermostat.configure_char(
CHAR_CURRENT_HUMIDITY, value=50
)

View File

@@ -2,12 +2,12 @@
import asyncio
from dataclasses import dataclass
import AIOSomecomfort
import aiosomecomfort
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
@@ -50,22 +50,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
username = config_entry.data[CONF_USERNAME]
password = config_entry.data[CONF_PASSWORD]
client = AIOSomecomfort.AIOSomeComfort(
client = aiosomecomfort.AIOSomeComfort(
username, password, session=async_get_clientsession(hass)
)
try:
await client.login()
await client.discover()
except AIOSomecomfort.AuthError as ex:
raise ConfigEntryNotReady(
"Failed to initialize the Honeywell client: "
"Check your configuration (username, password), "
) from ex
except aiosomecomfort.device.AuthError as ex:
raise ConfigEntryAuthFailed("Incorrect Password") from ex
except (
AIOSomecomfort.ConnectionError,
AIOSomecomfort.ConnectionTimeout,
aiosomecomfort.device.ConnectionError,
aiosomecomfort.device.ConnectionTimeout,
asyncio.TimeoutError,
) as ex:
raise ConfigEntryNotReady(
@@ -117,5 +114,5 @@ class HoneywellData:
"""Shared data for Honeywell."""
entry_id: str
client: AIOSomecomfort.AIOSomeComfort
devices: dict[str, AIOSomecomfort.device.Device]
client: aiosomecomfort.AIOSomeComfort
devices: dict[str, aiosomecomfort.device.Device]

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import datetime
from typing import Any
import AIOSomecomfort
import aiosomecomfort
from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH,
@@ -100,7 +100,7 @@ class HoneywellUSThermostat(ClimateEntity):
def __init__(
self,
data: HoneywellData,
device: AIOSomecomfort.device.Device,
device: aiosomecomfort.device.Device,
cool_away_temp: int | None,
heat_away_temp: int | None,
) -> None:
@@ -169,10 +169,17 @@ class HoneywellUSThermostat(ClimateEntity):
@property
def min_temp(self) -> float:
"""Return the minimum temperature."""
if self.hvac_mode in [HVACMode.COOL, HVACMode.HEAT_COOL]:
if self.hvac_mode == HVACMode.COOL:
return self._device.raw_ui_data["CoolLowerSetptLimit"]
if self.hvac_mode == HVACMode.HEAT:
return self._device.raw_ui_data["HeatLowerSetptLimit"]
if self.hvac_mode == HVACMode.HEAT_COOL:
return min(
[
self._device.raw_ui_data["CoolLowerSetptLimit"],
self._device.raw_ui_data["HeatLowerSetptLimit"],
]
)
return DEFAULT_MIN_TEMP
@property
@@ -180,8 +187,15 @@ class HoneywellUSThermostat(ClimateEntity):
"""Return the maximum temperature."""
if self.hvac_mode == HVACMode.COOL:
return self._device.raw_ui_data["CoolUpperSetptLimit"]
if self.hvac_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL]:
if self.hvac_mode == HVACMode.HEAT:
return self._device.raw_ui_data["HeatUpperSetptLimit"]
if self.hvac_mode == HVACMode.HEAT_COOL:
return max(
[
self._device.raw_ui_data["CoolUpperSetptLimit"],
self._device.raw_ui_data["HeatUpperSetptLimit"],
]
)
return DEFAULT_MAX_TEMP
@property
@@ -257,41 +271,45 @@ class HoneywellUSThermostat(ClimateEntity):
# Get current mode
mode = self._device.system_mode
# Set hold if this is not the case
if getattr(self._device, f"hold_{mode}", None) is False:
# Get next period key
next_period_key = f"{mode.capitalize()}NextPeriod"
# Get next period raw value
next_period = self._device.raw_ui_data.get(next_period_key)
if self._device.hold_heat is False and self._device.hold_cool is False:
# Get next period time
hour, minute = divmod(next_period * 15, 60)
hour_heat, minute_heat = divmod(
self._device.raw_ui_data["HeatNextPeriod"] * 15, 60
)
hour_cool, minute_cool = divmod(
self._device.raw_ui_data["CoolNextPeriod"] * 15, 60
)
# Set hold time
if mode in COOLING_MODES:
await self._device.set_hold_cool(datetime.time(hour, minute))
elif mode in HEATING_MODES:
await self._device.set_hold_heat(datetime.time(hour, minute))
await self._device.set_hold_cool(
datetime.time(hour_cool, minute_cool)
)
if mode in HEATING_MODES:
await self._device.set_hold_heat(
datetime.time(hour_heat, minute_heat)
)
# Set temperature
if mode in COOLING_MODES:
# Set temperature if not in auto
if mode == "cool":
await self._device.set_setpoint_cool(temperature)
elif mode in HEATING_MODES:
if mode == "heat":
await self._device.set_setpoint_heat(temperature)
except AIOSomecomfort.SomeComfortError:
_LOGGER.error("Temperature %.1f out of range", temperature)
except aiosomecomfort.SomeComfortError as err:
_LOGGER.error("Invalid temperature %.1f: %s", temperature, err)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if {HVACMode.COOL, HVACMode.HEAT} & set(self._hvac_mode_map):
await self._set_temperature(**kwargs)
try:
if HVACMode.HEAT_COOL in self._hvac_mode_map:
try:
if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH):
await self._device.set_setpoint_cool(temperature)
if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW):
await self._device.set_setpoint_heat(temperature)
except AIOSomecomfort.SomeComfortError as err:
_LOGGER.error("Invalid temperature %s: %s", temperature, err)
except aiosomecomfort.SomeComfortError as err:
_LOGGER.error("Invalid temperature %.1f: %s", temperature, err)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
@@ -312,7 +330,7 @@ class HoneywellUSThermostat(ClimateEntity):
try:
# Get current mode
mode = self._device.system_mode
except AIOSomecomfort.SomeComfortError:
except aiosomecomfort.SomeComfortError:
_LOGGER.error("Can not get system mode")
return
try:
@@ -322,12 +340,11 @@ class HoneywellUSThermostat(ClimateEntity):
if mode in COOLING_MODES:
await self._device.set_hold_cool(True)
await self._device.set_setpoint_cool(self._cool_away_temp)
elif mode in HEATING_MODES:
if mode in HEATING_MODES:
await self._device.set_hold_heat(True)
await self._device.set_setpoint_heat(self._heat_away_temp)
except AIOSomecomfort.SomeComfortError:
except aiosomecomfort.SomeComfortError:
_LOGGER.error(
"Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f",
mode,
@@ -340,7 +357,7 @@ class HoneywellUSThermostat(ClimateEntity):
try:
# Get current mode
mode = self._device.system_mode
except AIOSomecomfort.SomeComfortError:
except aiosomecomfort.SomeComfortError:
_LOGGER.error("Can not get system mode")
return
# Check that we got a valid mode back
@@ -349,10 +366,10 @@ class HoneywellUSThermostat(ClimateEntity):
# Set permanent hold
if mode in COOLING_MODES:
await self._device.set_hold_cool(True)
elif mode in HEATING_MODES:
if mode in HEATING_MODES:
await self._device.set_hold_heat(True)
except AIOSomecomfort.SomeComfortError:
except aiosomecomfort.SomeComfortError:
_LOGGER.error("Couldn't set permanent hold")
else:
_LOGGER.error("Invalid system mode returned: %s", mode)
@@ -364,7 +381,7 @@ class HoneywellUSThermostat(ClimateEntity):
# Disabling all hold modes
await self._device.set_hold_cool(False)
await self._device.set_hold_heat(False)
except AIOSomecomfort.SomeComfortError:
except aiosomecomfort.SomeComfortError:
_LOGGER.error("Can not stop hold mode")
async def async_set_preset_mode(self, preset_mode: str) -> None:
@@ -393,13 +410,13 @@ class HoneywellUSThermostat(ClimateEntity):
try:
await self._device.refresh()
except (
AIOSomecomfort.SomeComfortError,
aiosomecomfort.SomeComfortError,
OSError,
):
try:
await self._data.client.login()
except AIOSomecomfort.SomeComfortError:
except aiosomecomfort.SomeComfortError:
self._attr_available = False
await self.hass.async_create_task(
self.hass.config_entries.async_reload(self._data.entry_id)

View File

@@ -2,8 +2,10 @@
from __future__ import annotations
import asyncio
from collections.abc import Mapping
from typing import Any
import AIOSomecomfort
import aiosomecomfort
import voluptuous as vol
from homeassistant import config_entries
@@ -20,11 +22,67 @@ from .const import (
DOMAIN,
)
REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a honeywell config flow."""
VERSION = 1
entry: config_entries.ConfigEntry | None
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle re-authentication with Honeywell."""
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm re-authentication with Honeywell."""
errors: dict[str, str] = {}
if user_input:
assert self.entry is not None
password = user_input[CONF_PASSWORD]
data = {
CONF_USERNAME: self.entry.data[CONF_USERNAME],
CONF_PASSWORD: password,
}
try:
await self.is_valid(
username=data[CONF_USERNAME], password=data[CONF_PASSWORD]
)
except aiosomecomfort.AuthError:
errors["base"] = "invalid_auth"
except (
aiosomecomfort.ConnectionError,
aiosomecomfort.ConnectionTimeout,
asyncio.TimeoutError,
):
errors["base"] = "cannot_connect"
else:
self.hass.config_entries.async_update_entry(
self.entry,
data={
**self.entry.data,
CONF_PASSWORD: password,
},
)
await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
data_schema=REAUTH_SCHEMA,
errors=errors,
)
async def async_step_user(self, user_input=None) -> FlowResult:
"""Create config entry. Show the setup form to the user."""
@@ -32,11 +90,11 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
await self.is_valid(**user_input)
except AIOSomecomfort.AuthError:
except aiosomecomfort.AuthError:
errors["base"] = "invalid_auth"
except (
AIOSomecomfort.ConnectionError,
AIOSomecomfort.ConnectionTimeout,
aiosomecomfort.ConnectionError,
aiosomecomfort.ConnectionTimeout,
asyncio.TimeoutError,
):
errors["base"] = "cannot_connect"
@@ -57,7 +115,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def is_valid(self, **kwargs) -> bool:
"""Check if login credentials are valid."""
client = AIOSomecomfort.AIOSomeComfort(
client = aiosomecomfort.AIOSomeComfort(
kwargs[CONF_USERNAME],
kwargs[CONF_PASSWORD],
session=async_get_clientsession(self.hass),

View File

@@ -3,7 +3,7 @@
"name": "Honeywell Total Connect Comfort (US)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/honeywell",
"requirements": ["aiosomecomfort==0.0.3"],
"requirements": ["aiosomecomfort==0.0.6"],
"codeowners": ["@rdfurman", "@mkmer"],
"iot_class": "cloud_polling",
"loggers": ["somecomfort"]

View File

@@ -5,7 +5,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from AIOSomecomfort.device import Device
from aiosomecomfort.device import Device
from homeassistant.components.sensor import (
SensorDeviceClass,

View File

@@ -20,7 +20,11 @@ import voluptuous as vol
from homeassistant import exceptions
from homeassistant.const import CONTENT_TYPE_JSON
from homeassistant.core import Context, is_callback
from homeassistant.helpers.json import JSON_ENCODE_EXCEPTIONS, json_bytes
from homeassistant.helpers.json import JSON_ENCODE_EXCEPTIONS, json_bytes, json_dumps
from homeassistant.util.json import (
find_paths_unserializable_data,
format_unserializable_data,
)
from .const import KEY_AUTHENTICATED, KEY_HASS
@@ -54,7 +58,12 @@ class HomeAssistantView:
try:
msg = json_bytes(result)
except JSON_ENCODE_EXCEPTIONS as err:
_LOGGER.error("Unable to serialize to JSON: %s\n%s", err, result)
_LOGGER.error(
"Unable to serialize to JSON. Bad data found at %s",
format_unserializable_data(
find_paths_unserializable_data(result, dump=json_dumps)
),
)
raise HTTPInternalServerError from err
response = web.Response(
body=msg,

View File

@@ -255,7 +255,7 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = {
FILTER_STATES: ["open", "closed", "closing", "opening", "stopped"],
FILTER_NODE_DEF_ID: ["DimmerMotorSwitch_ADV"],
FILTER_INSTEON_TYPE: [TYPE_CATEGORY_COVER],
FILTER_ZWAVE_CAT: [],
FILTER_ZWAVE_CAT: ["106", "107"],
},
Platform.LIGHT: {
FILTER_UOM: ["51"],

View File

@@ -313,7 +313,11 @@ def _generate_device_info(node: Node) -> DeviceInfo:
model += f" ({node.type})"
# Get extra information for Z-Wave Devices
if node.protocol == PROTO_ZWAVE and node.zwave_props.mfr_id != "0":
if (
node.protocol == PROTO_ZWAVE
and node.zwave_props
and node.zwave_props.mfr_id != "0"
):
device_info[
ATTR_MANUFACTURER
] = f"Z-Wave MfrID:{int(node.zwave_props.mfr_id):#0{6}x}"

View File

@@ -3,7 +3,7 @@
"name": "Universal Devices ISY/IoX",
"integration_type": "hub",
"documentation": "https://www.home-assistant.io/integrations/isy994",
"requirements": ["pyisy==3.1.9"],
"requirements": ["pyisy==3.1.11"],
"codeowners": ["@bdraco", "@shbatm"],
"config_flow": true,
"ssdp": [

View File

@@ -3,7 +3,7 @@
"name": "Keenetic NDMS2 Router",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2",
"requirements": ["ndms2_client==0.1.1"],
"requirements": ["ndms2_client==0.1.2"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1",

View File

@@ -3,7 +3,7 @@
"name": "Matter (BETA)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/matter",
"requirements": ["python-matter-server==2.0.1"],
"requirements": ["python-matter-server==2.0.2"],
"dependencies": ["websocket_api"],
"codeowners": ["@home-assistant/matter"],
"iot_class": "local_push"

View File

@@ -1,6 +1,7 @@
"""Sensor platform for mobile_app."""
from __future__ import annotations
from datetime import date, datetime
from typing import Any
from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass
@@ -10,6 +11,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util
from .const import (
@@ -99,7 +101,7 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor):
self._config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement
@property
def native_value(self):
def native_value(self) -> StateType | date | datetime:
"""Return the state of the sensor."""
if (state := self._config[ATTR_SENSOR_STATE]) in (None, STATE_UNKNOWN):
return None
@@ -122,7 +124,7 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor):
return state
@property
def native_unit_of_measurement(self):
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement this sensor expresses itself in."""
return self._config.get(ATTR_SENSOR_UOM)

View File

@@ -22,9 +22,7 @@ from homeassistant.components import (
notify as hass_notify,
tag,
)
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES as BINARY_SENSOR_CLASSES,
)
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.device_tracker import (
ATTR_BATTERY,
@@ -33,10 +31,7 @@ from homeassistant.components.device_tracker import (
ATTR_LOCATION_NAME,
)
from homeassistant.components.frontend import MANIFEST_JSON
from homeassistant.components.sensor import (
DEVICE_CLASSES as SENSOR_CLASSES,
STATE_CLASSES as SENSOSR_STATE_CLASSES,
)
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -58,7 +53,7 @@ from homeassistant.helpers import (
template,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA
from homeassistant.helpers.entity import EntityCategory
from homeassistant.util.decorator import Registry
from .const import (
@@ -131,8 +126,7 @@ WEBHOOK_COMMANDS: Registry[
str, Callable[[HomeAssistant, ConfigEntry, Any], Coroutine[Any, Any, Response]]
] = Registry()
COMBINED_CLASSES = set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES)
SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR]
SENSOR_TYPES = (ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR)
WEBHOOK_PAYLOAD_SCHEMA = vol.Schema(
{
@@ -507,19 +501,27 @@ def _extract_sensor_unique_id(webhook_id: str, unique_id: str) -> str:
vol.All(
{
vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict,
vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All(
vol.Lower, vol.In(COMBINED_CLASSES)
vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.Any(
None,
vol.All(vol.Lower, vol.Coerce(BinarySensorDeviceClass)),
vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)),
),
vol.Required(ATTR_SENSOR_NAME): cv.string,
vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES),
vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string,
vol.Optional(ATTR_SENSOR_UOM): cv.string,
vol.Optional(ATTR_SENSOR_UOM): vol.Any(None, cv.string),
vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any(
None, bool, str, int, float
None, bool, int, float, str
),
vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): vol.Any(
None, vol.Coerce(EntityCategory)
),
vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any(
None, cv.icon
),
vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.Any(
None, vol.Coerce(SensorStateClass)
),
vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon,
vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.In(SENSOSR_STATE_CLASSES),
vol.Optional(ATTR_SENSOR_DISABLED): bool,
},
_validate_state_class_sensor,
@@ -619,8 +621,10 @@ async def webhook_update_sensor_states(
sensor_schema_full = vol.Schema(
{
vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict,
vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon,
vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, str, int, float),
vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any(
None, cv.icon
),
vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, int, float, str),
vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES),
vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string,
}

View File

@@ -19,6 +19,7 @@ from homeassistant.components.sensor import (
from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfElectricPotential,
UnitOfLength,
UnitOfTemperature,
)
@@ -41,7 +42,7 @@ SENSOR_DESCRIPTIONS = {
"battery_voltage": SensorEntityDescription(
key="battery_voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,

View File

@@ -744,8 +744,12 @@ class MqttDiscoveryDeviceUpdate(ABC):
self.log_name,
discovery_hash,
)
await self.async_update(discovery_payload)
if not discovery_payload:
try:
await self.async_update(discovery_payload)
finally:
send_discovery_done(self.hass, self._discovery_data)
self._discovery_data[ATTR_DISCOVERY_PAYLOAD] = discovery_payload
elif not discovery_payload:
# Unregister and clean up the current discovery instance
stop_discovery_updates(
self.hass, self._discovery_data, self._remove_discovery_updated
@@ -869,15 +873,19 @@ class MqttDiscoveryUpdate(Entity):
_LOGGER.info("Removing component: %s", self.entity_id)
self._cleanup_discovery_on_remove()
await _async_remove_state_and_registry_entry(self)
send_discovery_done(self.hass, self._discovery_data)
elif self._discovery_update:
if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]:
# Non-empty, changed payload: Notify component
_LOGGER.info("Updating component: %s", self.entity_id)
await self._discovery_update(payload)
try:
await self._discovery_update(payload)
finally:
send_discovery_done(self.hass, self._discovery_data)
else:
# Non-empty, unchanged payload: Ignore to avoid changing states
_LOGGER.debug("Ignoring unchanged update for: %s", self.entity_id)
send_discovery_done(self.hass, self._discovery_data)
send_discovery_done(self.hass, self._discovery_data)
if discovery_hash:
assert self._discovery_data is not None

View File

@@ -13,7 +13,7 @@ from homeassistant import exceptions
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import device_registry, entity_registry
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
@@ -41,37 +41,6 @@ def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOp
return bridge.locks, bridge.openers
def _update_devices(devices: list[NukiDevice]) -> dict[str, set[str]]:
"""
Update the Nuki devices.
Returns:
A dict with the events to be fired. The event type is the key and the device ids are the value
"""
events: dict[str, set[str]] = defaultdict(set)
for device in devices:
for level in (False, True):
try:
if isinstance(device, NukiOpener):
last_ring_action_state = device.ring_action_state
device.update(level)
if not last_ring_action_state and device.ring_action_state:
events["ring"].add(device.nuki_id)
else:
device.update(level)
except RequestException:
continue
if device.state not in ERROR_STATES:
break
return events
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Nuki entry."""
@@ -101,42 +70,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except RequestException as err:
raise exceptions.ConfigEntryNotReady from err
async def async_update_data() -> None:
"""Fetch data from Nuki bridge."""
try:
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
# handled by the data update coordinator.
async with async_timeout.timeout(10):
events = await hass.async_add_executor_job(
_update_devices, locks + openers
)
except InvalidCredentialsException as err:
raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err
except RequestException as err:
raise UpdateFailed(f"Error communicating with Bridge: {err}") from err
ent_reg = er.async_get(hass)
for event, device_ids in events.items():
for device_id in device_ids:
entity_id = ent_reg.async_get_entity_id(
Platform.LOCK, DOMAIN, device_id
)
event_data = {
"entity_id": entity_id,
"type": event,
}
hass.bus.async_fire("nuki_event", event_data)
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name="nuki devices",
update_method=async_update_data,
# Polling interval. Will only be polled if there are subscribers.
update_interval=UPDATE_INTERVAL,
# Device registration for the bridge
info = bridge.info()
bridge_id = parse_id(info["ids"]["hardwareId"])
dev_reg = device_registry.async_get(hass)
dev_reg.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, bridge_id)},
manufacturer="Nuki Home Solutions GmbH",
name=f"Nuki Bridge {bridge_id}",
model="Hardware Bridge",
sw_version=info["versions"]["firmwareVersion"],
)
coordinator = NukiCoordinator(hass, bridge, locks, openers)
hass.data[DOMAIN][entry.entry_id] = {
DATA_COORDINATOR: coordinator,
DATA_BRIDGE: bridge,
@@ -178,3 +126,94 @@ class NukiEntity(CoordinatorEntity[DataUpdateCoordinator[None]]):
"""Pass coordinator to CoordinatorEntity."""
super().__init__(coordinator)
self._nuki_device = nuki_device
@property
def device_info(self):
"""Device info for Nuki entities."""
return {
"identifiers": {(DOMAIN, parse_id(self._nuki_device.nuki_id))},
"name": self._nuki_device.name,
"manufacturer": "Nuki Home Solutions GmbH",
"model": self._nuki_device.device_type_str.capitalize(),
"sw_version": self._nuki_device.firmware_version,
"via_device": (DOMAIN, self.coordinator.bridge_id),
}
class NukiCoordinator(DataUpdateCoordinator):
"""Data Update Coordinator for the Nuki integration."""
def __init__(self, hass, bridge, locks, openers):
"""Initialize my coordinator."""
super().__init__(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name="nuki devices",
# Polling interval. Will only be polled if there are subscribers.
update_interval=UPDATE_INTERVAL,
)
self.bridge = bridge
self.locks = locks
self.openers = openers
@property
def bridge_id(self):
"""Return the parsed id of the Nuki bridge."""
return parse_id(self.bridge.info()["ids"]["hardwareId"])
async def _async_update_data(self) -> None:
"""Fetch data from Nuki bridge."""
try:
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
# handled by the data update coordinator.
async with async_timeout.timeout(10):
events = await self.hass.async_add_executor_job(
self.update_devices, self.locks + self.openers
)
except InvalidCredentialsException as err:
raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err
except RequestException as err:
raise UpdateFailed(f"Error communicating with Bridge: {err}") from err
ent_reg = entity_registry.async_get(self.hass)
for event, device_ids in events.items():
for device_id in device_ids:
entity_id = ent_reg.async_get_entity_id(
Platform.LOCK, DOMAIN, device_id
)
event_data = {
"entity_id": entity_id,
"type": event,
}
self.hass.bus.async_fire("nuki_event", event_data)
def update_devices(self, devices: list[NukiDevice]) -> dict[str, set[str]]:
"""
Update the Nuki devices.
Returns:
A dict with the events to be fired. The event type is the key and the device ids are the value
"""
events: dict[str, set[str]] = defaultdict(set)
for device in devices:
for level in (False, True):
try:
if isinstance(device, NukiOpener):
last_ring_action_state = device.ring_action_state
device.update(level)
if not last_ring_action_state and device.ring_action_state:
events["ring"].add(device.nuki_id)
else:
device.update(level)
except RequestException:
continue
if device.state not in ERROR_STATES:
break
return events

View File

@@ -34,13 +34,10 @@ async def async_setup_entry(
class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity):
"""Representation of a Nuki Lock Doorsensor."""
_attr_has_entity_name = True
_attr_name = "Door sensor"
_attr_device_class = BinarySensorDeviceClass.DOOR
@property
def name(self):
"""Return the name of the lock."""
return self._nuki_device.name
@property
def unique_id(self) -> str:
"""Return a unique ID."""

View File

@@ -67,13 +67,9 @@ async def async_setup_entry(
class NukiDeviceEntity(NukiEntity, LockEntity, ABC):
"""Representation of a Nuki device."""
_attr_has_entity_name = True
_attr_supported_features = LockEntityFeature.OPEN
@property
def name(self) -> str | None:
"""Return the name of the lock."""
return self._nuki_device.name
@property
def unique_id(self) -> str | None:
"""Return a unique ID."""

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
from functools import partial
import logging
from typing import cast
import openai
from openai import error
@@ -13,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, TemplateError
from homeassistant.helpers import area_registry, device_registry, intent, template
from homeassistant.helpers import area_registry, intent, template
from homeassistant.util import ulid
from .const import DEFAULT_MODEL, DEFAULT_PROMPT
@@ -74,6 +73,7 @@ class OpenAIAgent(conversation.AbstractConversationAgent):
try:
prompt = self._async_generate_prompt()
except TemplateError as err:
_LOGGER.error("Error rendering prompt: %s", err)
intent_response = intent.IntentResponse(language=user_input.language)
intent_response.async_set_error(
intent.IntentResponseErrorCode.UNKNOWN,
@@ -97,15 +97,26 @@ class OpenAIAgent(conversation.AbstractConversationAgent):
_LOGGER.debug("Prompt for %s: %s", model, prompt)
result = await self.hass.async_add_executor_job(
partial(
openai.Completion.create,
engine=model,
prompt=prompt,
max_tokens=150,
user=conversation_id,
try:
result = await self.hass.async_add_executor_job(
partial(
openai.Completion.create,
engine=model,
prompt=prompt,
max_tokens=150,
user=conversation_id,
)
)
)
except error.OpenAIError as err:
intent_response = intent.IntentResponse(language=user_input.language)
intent_response.async_set_error(
intent.IntentResponseErrorCode.UNKNOWN,
f"Sorry, I had a problem talking to OpenAI: {err}",
)
return conversation.ConversationResult(
response=intent_response, conversation_id=conversation_id
)
_LOGGER.debug("Response %s", result)
response = result["choices"][0]["text"].strip()
self.history[conversation_id] = prompt + response
@@ -122,20 +133,9 @@ class OpenAIAgent(conversation.AbstractConversationAgent):
def _async_generate_prompt(self) -> str:
"""Generate a prompt for the user."""
dev_reg = device_registry.async_get(self.hass)
return template.Template(DEFAULT_PROMPT, self.hass).async_render(
{
"ha_name": self.hass.config.location_name,
"areas": [
area
for area in area_registry.async_get(self.hass).areas.values()
# Filter out areas without devices
if any(
not dev.disabled_by
for dev in device_registry.async_entries_for_area(
dev_reg, cast(str, area.id)
)
)
],
"areas": list(area_registry.async_get(self.hass).areas.values()),
}
)

View File

@@ -3,19 +3,26 @@
DOMAIN = "openai_conversation"
CONF_PROMPT = "prompt"
DEFAULT_MODEL = "text-davinci-003"
DEFAULT_PROMPT = """
You are a conversational AI for a smart home named {{ ha_name }}.
If a user wants to control a device, reject the request and suggest using the Home Assistant UI.
DEFAULT_PROMPT = """This smart home is controlled by Home Assistant.
An overview of the areas and the devices in this smart home:
{% for area in areas %}
{%- for area in areas %}
{%- set area_info = namespace(printed=false) %}
{%- for device in area_devices(area.name) -%}
{%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") %}
{%- if not area_info.printed %}
{{ area.name }}:
{% for device in area_devices(area.name) -%}
{%- if not device_attr(device, "disabled_by") %}
- {{ device_attr(device, "name") }} ({{ device_attr(device, "model") }} by {{ device_attr(device, "manufacturer") }})
{%- endif %}
{%- set area_info.printed = true %}
{%- endif %}
- {{ device_attr(device, "name") }}{% if device_attr(device, "model") and device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %}
{%- endif %}
{%- endfor %}
{%- endfor %}
{% endfor %}
Answer the users questions about the world truthfully.
If the user wants to control a device, reject the request and suggest using the Home Assistant app.
Now finish this conversation:

View File

@@ -386,7 +386,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
key=OverkizState.CORE_THREE_WAY_HANDLE_DIRECTION,
name="Three way handle direction",
device_class=SensorDeviceClass.ENUM,
options=["open", "tilt", "close"],
options=["open", "tilt", "closed"],
translation_key="three_way_handle_direction",
),
]

View File

@@ -2,7 +2,7 @@
"domain": "plugwise",
"name": "Plugwise",
"documentation": "https://www.home-assistant.io/integrations/plugwise",
"requirements": ["plugwise==0.27.1"],
"requirements": ["plugwise==0.27.5"],
"codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"],
"zeroconf": ["_plugwise._tcp.local."],
"config_flow": true,

View File

@@ -19,7 +19,7 @@ EVENT_RECORDER_HOURLY_STATISTICS_GENERATED = "recorder_hourly_statistics_generat
CONF_DB_INTEGRITY_CHECK = "db_integrity_check"
MAX_QUEUE_BACKLOG = 40000
MAX_QUEUE_BACKLOG = 65000
# The maximum number of rows (events) we purge in one delete statement

View File

@@ -836,7 +836,9 @@ class Recorder(threading.Thread):
return
try:
shared_data_bytes = EventData.shared_data_bytes_from_event(event)
shared_data_bytes = EventData.shared_data_bytes_from_event(
event, self.dialect_name
)
except JSON_ENCODE_EXCEPTIONS as ex:
_LOGGER.warning("Event is not JSON serializable: %s: %s", event, ex)
return
@@ -869,7 +871,7 @@ class Recorder(threading.Thread):
try:
dbstate = States.from_event(event)
shared_attrs_bytes = StateAttributes.shared_attrs_bytes_from_event(
event, self._exclude_attributes_by_domain
event, self._exclude_attributes_by_domain, self.dialect_name
)
except JSON_ENCODE_EXCEPTIONS as ex:
_LOGGER.warning(
@@ -1024,7 +1026,9 @@ class Recorder(threading.Thread):
def _post_schema_migration(self, old_version: int, new_version: int) -> None:
"""Run post schema migration tasks."""
migration.post_schema_migration(self.event_session, old_version, new_version)
migration.post_schema_migration(
self.engine, self.event_session, old_version, new_version
)
def _send_keep_alive(self) -> None:
"""Send a keep alive to keep the db connection open."""
@@ -1040,6 +1044,8 @@ class Recorder(threading.Thread):
async def async_block_till_done(self) -> None:
"""Async version of block_till_done."""
if self._queue.empty() and not self._event_session_has_pending_writes():
return
event = asyncio.Event()
self.queue_task(SynchronizeTask(event))
await event.wait()

View File

@@ -43,18 +43,19 @@ from homeassistant.helpers.json import (
JSON_DECODE_EXCEPTIONS,
JSON_DUMP,
json_bytes,
json_bytes_strip_null,
json_loads,
)
import homeassistant.util.dt as dt_util
from .const import ALL_DOMAIN_EXCLUDE_ATTRS
from .const import ALL_DOMAIN_EXCLUDE_ATTRS, SupportedDialect
from .models import StatisticData, StatisticMetaData, process_timestamp
# SQLAlchemy Schema
# pylint: disable=invalid-name
Base = declarative_base()
SCHEMA_VERSION = 32
SCHEMA_VERSION = 33
_StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase")
@@ -251,8 +252,12 @@ class EventData(Base): # type: ignore[misc,valid-type]
)
@staticmethod
def shared_data_bytes_from_event(event: Event) -> bytes:
def shared_data_bytes_from_event(
event: Event, dialect: SupportedDialect | None
) -> bytes:
"""Create shared_data from an event."""
if dialect == SupportedDialect.POSTGRESQL:
return json_bytes_strip_null(event.data)
return json_bytes(event.data)
@staticmethod
@@ -416,7 +421,9 @@ class StateAttributes(Base): # type: ignore[misc,valid-type]
@staticmethod
def shared_attrs_bytes_from_event(
event: Event, exclude_attrs_by_domain: dict[str, set[str]]
event: Event,
exclude_attrs_by_domain: dict[str, set[str]],
dialect: SupportedDialect | None,
) -> bytes:
"""Create shared_attrs from a state_changed event."""
state: State | None = event.data.get("new_state")
@@ -427,6 +434,10 @@ class StateAttributes(Base): # type: ignore[misc,valid-type]
exclude_attrs = (
exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS
)
if dialect == SupportedDialect.POSTGRESQL:
return json_bytes_strip_null(
{k: v for k, v in state.attributes.items() if k not in exclude_attrs}
)
return json_bytes(
{k: v for k, v in state.attributes.items() if k not in exclude_attrs}
)

View File

@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING
import sqlalchemy
from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text
from sqlalchemy.engine import Engine
from sqlalchemy.engine import CursorResult, Engine
from sqlalchemy.exc import (
DatabaseError,
InternalError,
@@ -43,7 +43,7 @@ from .statistics import (
get_start_time,
validate_db_schema as statistics_validate_db_schema,
)
from .tasks import PostSchemaMigrationTask
from .tasks import CommitTask, PostSchemaMigrationTask
from .util import session_scope
if TYPE_CHECKING:
@@ -166,6 +166,9 @@ def migrate_schema(
if current_version != SCHEMA_VERSION:
instance.queue_task(PostSchemaMigrationTask(current_version, SCHEMA_VERSION))
# Make sure the post schema migration task is committed in case
# the next task does not have commit_before = True
instance.queue_task(CommitTask())
def _create_index(
@@ -271,9 +274,13 @@ def _drop_index(
"Finished dropping index %s from table %s", index_name, table_name
)
else:
if index_name == "ix_states_context_parent_id":
# Was only there on nightly so we do not want
if index_name in ("ix_states_entity_id", "ix_states_context_parent_id"):
# ix_states_context_parent_id was only there on nightly so we do not want
# to generate log noise or issues about it.
#
# ix_states_entity_id was only there for users who upgraded from schema
# version 8 or earlier. Newer installs will not have it so we do not
# want to generate log noise or issues about it.
return
_LOGGER.warning(
@@ -502,12 +509,12 @@ def _apply_update( # noqa: C901
timestamp_type = "FLOAT"
if new_version == 1:
_create_index(session_maker, "events", "ix_events_time_fired")
# This used to create ix_events_time_fired, but it was removed in version 32
pass
elif new_version == 2:
# Create compound start/end index for recorder_runs
_create_index(session_maker, "recorder_runs", "ix_recorder_runs_start_end")
# Create indexes for states
_create_index(session_maker, "states", "ix_states_last_updated")
# This used to create ix_states_last_updated bit it was removed in version 32
elif new_version == 3:
# There used to be a new index here, but it was removed in version 4.
pass
@@ -526,8 +533,7 @@ def _apply_update( # noqa: C901
_drop_index(session_maker, "states", "states__state_changes")
_drop_index(session_maker, "states", "states__significant_changes")
_drop_index(session_maker, "states", "ix_states_entity_id_created")
_create_index(session_maker, "states", "ix_states_entity_id_last_updated")
# This used to create ix_states_entity_id_last_updated, but it was removed in version 32
elif new_version == 5:
# Create supporting index for States.event_id foreign key
_create_index(session_maker, "states", "ix_states_event_id")
@@ -538,20 +544,21 @@ def _apply_update( # noqa: C901
["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"],
)
_create_index(session_maker, "events", "ix_events_context_id")
_create_index(session_maker, "events", "ix_events_context_user_id")
# This used to create ix_events_context_user_id, but it was removed in version 28
_add_columns(
session_maker,
"states",
["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"],
)
_create_index(session_maker, "states", "ix_states_context_id")
_create_index(session_maker, "states", "ix_states_context_user_id")
# This used to create ix_states_context_user_id, but it was removed in version 28
elif new_version == 7:
_create_index(session_maker, "states", "ix_states_entity_id")
# There used to be a ix_states_entity_id index here, but it was removed in later schema
pass
elif new_version == 8:
_add_columns(session_maker, "events", ["context_parent_id CHARACTER(36)"])
_add_columns(session_maker, "states", ["old_state_id INTEGER"])
_create_index(session_maker, "events", "ix_events_context_parent_id")
# This used to create ix_events_context_parent_id, but it was removed in version 28
elif new_version == 9:
# We now get the context from events with a join
# since its always there on state_changed events
@@ -569,7 +576,7 @@ def _apply_update( # noqa: C901
# Redundant keys on composite index:
# We already have ix_states_entity_id_last_updated
_drop_index(session_maker, "states", "ix_states_entity_id")
_create_index(session_maker, "events", "ix_events_event_type_time_fired")
# This used to create ix_events_event_type_time_fired, but it was removed in version 32
_drop_index(session_maker, "events", "ix_events_event_type")
elif new_version == 10:
# Now done in step 11
@@ -846,8 +853,7 @@ def _apply_update( # noqa: C901
_create_index(session_maker, "events", "ix_events_event_type_time_fired_ts")
_create_index(session_maker, "states", "ix_states_entity_id_last_updated_ts")
_create_index(session_maker, "states", "ix_states_last_updated_ts")
with session_scope(session=session_maker()) as session:
_migrate_columns_to_timestamp(hass, session, engine)
_migrate_columns_to_timestamp(session_maker, engine)
elif new_version == 32:
# Migration is done in two steps to ensure we can start using
# the new columns before we wipe the old ones.
@@ -855,11 +861,17 @@ def _apply_update( # noqa: C901
_drop_index(session_maker, "events", "ix_events_event_type_time_fired")
_drop_index(session_maker, "states", "ix_states_last_updated")
_drop_index(session_maker, "events", "ix_events_time_fired")
elif new_version == 33:
# This index is no longer used and can cause MySQL to use the wrong index
# when querying the states table.
# https://github.com/home-assistant/core/issues/83787
_drop_index(session_maker, "states", "ix_states_entity_id")
else:
raise ValueError(f"No schema migration defined for version {new_version}")
def post_schema_migration(
engine: Engine,
session: Session,
old_version: int,
new_version: int,
@@ -878,62 +890,147 @@ def post_schema_migration(
# In version 31 we migrated all the time_fired, last_updated, and last_changed
# columns to be timestamps. In version 32 we need to wipe the old columns
# since they are no longer used and take up a significant amount of space.
_wipe_old_string_time_columns(session)
_wipe_old_string_time_columns(engine, session)
def _wipe_old_string_time_columns(session: Session) -> None:
def _wipe_old_string_time_columns(engine: Engine, session: Session) -> None:
"""Wipe old string time columns to save space."""
# Wipe Events.time_fired since its been replaced by Events.time_fired_ts
# Wipe States.last_updated since its been replaced by States.last_updated_ts
# Wipe States.last_changed since its been replaced by States.last_changed_ts
session.execute(text("UPDATE events set time_fired=NULL;"))
session.execute(text("UPDATE states set last_updated=NULL, last_changed=NULL;"))
session.commit()
#
if engine.dialect.name == SupportedDialect.SQLITE:
session.execute(text("UPDATE events set time_fired=NULL;"))
session.commit()
session.execute(text("UPDATE states set last_updated=NULL, last_changed=NULL;"))
session.commit()
elif engine.dialect.name == SupportedDialect.MYSQL:
#
# Since this is only to save space we limit the number of rows we update
# to 10,000,000 per table since we do not want to block the database for too long
# or run out of innodb_buffer_pool_size on MySQL. The old data will eventually
# be cleaned up by the recorder purge if we do not do it now.
#
session.execute(text("UPDATE events set time_fired=NULL LIMIT 10000000;"))
session.commit()
session.execute(
text(
"UPDATE states set last_updated=NULL, last_changed=NULL "
" LIMIT 10000000;"
)
)
session.commit()
elif engine.dialect.name == SupportedDialect.POSTGRESQL:
#
# Since this is only to save space we limit the number of rows we update
# to 250,000 per table since we do not want to block the database for too long
# or run out ram with postgresql. The old data will eventually
# be cleaned up by the recorder purge if we do not do it now.
#
session.execute(
text(
"UPDATE events set time_fired=NULL "
"where event_id in "
"(select event_id from events where time_fired_ts is NOT NULL LIMIT 250000);"
)
)
session.commit()
session.execute(
text(
"UPDATE states set last_updated=NULL, last_changed=NULL "
"where state_id in "
"(select state_id from states where last_updated_ts is NOT NULL LIMIT 250000);"
)
)
session.commit()
def _migrate_columns_to_timestamp(
hass: HomeAssistant, session: Session, engine: Engine
session_maker: Callable[[], Session], engine: Engine
) -> None:
"""Migrate columns to use timestamp."""
# Migrate all data in Events.time_fired to Events.time_fired_ts
# Migrate all data in States.last_updated to States.last_updated_ts
# Migrate all data in States.last_changed to States.last_changed_ts
connection = session.connection()
result: CursorResult | None = None
if engine.dialect.name == SupportedDialect.SQLITE:
connection.execute(
text(
'UPDATE events set time_fired_ts=strftime("%s",time_fired) + '
"cast(substr(time_fired,-7) AS FLOAT);"
# With SQLite we do this in one go since it is faster
with session_scope(session=session_maker()) as session:
connection = session.connection()
connection.execute(
text(
'UPDATE events set time_fired_ts=strftime("%s",time_fired) + '
"cast(substr(time_fired,-7) AS FLOAT);"
)
)
)
connection.execute(
text(
'UPDATE states set last_updated_ts=strftime("%s",last_updated) + '
"cast(substr(last_updated,-7) AS FLOAT), "
'last_changed_ts=strftime("%s",last_changed) + '
"cast(substr(last_changed,-7) AS FLOAT);"
connection.execute(
text(
'UPDATE states set last_updated_ts=strftime("%s",last_updated) + '
"cast(substr(last_updated,-7) AS FLOAT), "
'last_changed_ts=strftime("%s",last_changed) + '
"cast(substr(last_changed,-7) AS FLOAT);"
)
)
)
elif engine.dialect.name == SupportedDialect.MYSQL:
connection.execute(
text("UPDATE events set time_fired_ts=UNIX_TIMESTAMP(time_fired);")
)
connection.execute(
text(
"UPDATE states set last_updated_ts=UNIX_TIMESTAMP(last_updated), "
"last_changed_ts=UNIX_TIMESTAMP(last_changed);"
)
)
# With MySQL we do this in chunks to avoid hitting the `innodb_buffer_pool_size` limit
# We also need to do this in a loop since we can't be sure that we have
# updated all rows in the table until the rowcount is 0
while result is None or result.rowcount > 0:
with session_scope(session=session_maker()) as session:
result = session.connection().execute(
text(
"UPDATE events set time_fired_ts="
"IF(time_fired is NULL,0,"
"UNIX_TIMESTAMP(CONVERT_TZ(time_fired,'+00:00',@@global.time_zone))"
") "
"where time_fired_ts is NULL "
"LIMIT 250000;"
)
)
result = None
while result is None or result.rowcount > 0:
with session_scope(session=session_maker()) as session:
result = session.connection().execute(
text(
"UPDATE states set last_updated_ts="
"IF(last_updated is NULL,0,"
"UNIX_TIMESTAMP(CONVERT_TZ(last_updated,'+00:00',@@global.time_zone)) "
"), "
"last_changed_ts="
"UNIX_TIMESTAMP(CONVERT_TZ(last_changed,'+00:00',@@global.time_zone)) "
"where last_updated_ts is NULL "
"LIMIT 250000;"
)
)
elif engine.dialect.name == SupportedDialect.POSTGRESQL:
connection.execute(
text("UPDATE events set time_fired_ts=EXTRACT(EPOCH FROM time_fired);")
)
connection.execute(
text(
"UPDATE states set last_updated_ts=EXTRACT(EPOCH FROM last_updated), "
"last_changed_ts=EXTRACT(EPOCH FROM last_changed);"
)
)
# With Postgresql we do this in chunks to avoid using too much memory
# We also need to do this in a loop since we can't be sure that we have
# updated all rows in the table until the rowcount is 0
while result is None or result.rowcount > 0:
with session_scope(session=session_maker()) as session:
result = session.connection().execute(
text(
"UPDATE events SET "
"time_fired_ts= "
"(case when time_fired is NULL then 0 else EXTRACT(EPOCH FROM time_fired) end) "
"WHERE event_id IN ( "
"SELECT event_id FROM events where time_fired_ts is NULL LIMIT 250000 "
" );"
)
)
result = None
while result is None or result.rowcount > 0:
with session_scope(session=session_maker()) as session:
result = session.connection().execute(
text(
"UPDATE states set last_updated_ts="
"(case when last_updated is NULL then 0 else EXTRACT(EPOCH FROM last_updated) end), "
"last_changed_ts=EXTRACT(EPOCH FROM last_changed) "
"where state_id IN ( "
"SELECT state_id FROM states where last_updated_ts is NULL LIMIT 250000 "
" );"
)
)
def _initialize_database(session: Session) -> bool:

View File

@@ -36,8 +36,12 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import (
BaseUnitConverter,
DataRateConverter,
DistanceConverter,
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter,
InformationConverter,
MassConverter,
PowerConverter,
PressureConverter,
@@ -128,8 +132,15 @@ QUERY_STATISTIC_META = [
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
**{unit: DataRateConverter for unit in DataRateConverter.VALID_UNITS},
**{unit: DistanceConverter for unit in DistanceConverter.VALID_UNITS},
**{unit: ElectricCurrentConverter for unit in ElectricCurrentConverter.VALID_UNITS},
**{
unit: ElectricPotentialConverter
for unit in ElectricPotentialConverter.VALID_UNITS
},
**{unit: EnergyConverter for unit in EnergyConverter.VALID_UNITS},
**{unit: InformationConverter for unit in InformationConverter.VALID_UNITS},
**{unit: MassConverter for unit in MassConverter.VALID_UNITS},
**{unit: PowerConverter for unit in PowerConverter.VALID_UNITS},
**{unit: PressureConverter for unit in PressureConverter.VALID_UNITS},

View File

@@ -7,5 +7,11 @@
"database_engine": "Database Engine",
"database_version": "Database Version"
}
},
"issues": {
"maria_db_range_index_regression": {
"title": "Update MariaDB to {min_version} or later resolve a significant performance issue",
"description": "Older versions of MariaDB suffer from a significant performance regression when retrieving history data or purging the database. Update to MariaDB version {min_version} or later and restart Home Assistant. If you are using the MariaDB core add-on, make sure to update it to the latest version."
}
}
}

View File

@@ -1,4 +1,10 @@
{
"issues": {
"maria_db_range_index_regression": {
"description": "Older versions of MariaDB suffer from a significant performance regression when retrieving history data or purging the database. Update to MariaDB version {min_version} or later and restart Home Assistant. If you are using the MariaDB core add-on, make sure to update it to the latest version.",
"title": "Update MariaDB to {min_version} or later resolve a significant performance issue"
}
},
"system_health": {
"info": {
"current_recorder_run": "Current Run Start Time",

View File

@@ -25,11 +25,11 @@ from sqlalchemy.orm.session import Session
from sqlalchemy.sql.lambdas import StatementLambdaElement
import voluptuous as vol
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, issue_registry as ir
import homeassistant.util.dt as dt_util
from .const import DATA_INSTANCE, SQLITE_URL_PREFIX, SupportedDialect
from .const import DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX, SupportedDialect
from .db_schema import (
TABLE_RECORDER_RUNS,
TABLE_SCHEMA_CHANGES,
@@ -51,9 +51,35 @@ QUERY_RETRY_WAIT = 0.1
SQLITE3_POSTFIXES = ["", "-wal", "-shm"]
DEFAULT_YIELD_STATES_ROWS = 32768
# Our minimum versions for each database
#
# Older MariaDB suffers https://jira.mariadb.org/browse/MDEV-25020
# which is fixed in 10.5.17, 10.6.9, 10.7.5, 10.8.4
#
MIN_VERSION_MARIA_DB = AwesomeVersion(
"10.3.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER
)
RECOMMENDED_MIN_VERSION_MARIA_DB = AwesomeVersion(
"10.5.17", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER
)
MARIA_DB_106 = AwesomeVersion(
"10.6.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER
)
RECOMMENDED_MIN_VERSION_MARIA_DB_106 = AwesomeVersion(
"10.6.9", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER
)
MARIA_DB_107 = AwesomeVersion(
"10.7.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER
)
RECOMMENDED_MIN_VERSION_MARIA_DB_107 = AwesomeVersion(
"10.7.5", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER
)
MARIA_DB_108 = AwesomeVersion(
"10.8.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER
)
RECOMMENDED_MIN_VERSION_MARIA_DB_108 = AwesomeVersion(
"10.8.4", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER
)
MIN_VERSION_MYSQL = AwesomeVersion(
"8.0.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER
)
@@ -408,6 +434,34 @@ def build_mysqldb_conv() -> dict:
return {**conversions, FIELD_TYPE.DATETIME: _datetime_or_none}
@callback
def _async_create_mariadb_range_index_regression_issue(
hass: HomeAssistant, version: AwesomeVersion
) -> None:
"""Create an issue for the index range regression in older MariaDB.
The range scan issue was fixed in MariaDB 10.5.17, 10.6.9, 10.7.5, 10.8.4 and later.
"""
if version >= MARIA_DB_108:
min_version = RECOMMENDED_MIN_VERSION_MARIA_DB_108
elif version >= MARIA_DB_107:
min_version = RECOMMENDED_MIN_VERSION_MARIA_DB_107
elif version >= MARIA_DB_106:
min_version = RECOMMENDED_MIN_VERSION_MARIA_DB_106
else:
min_version = RECOMMENDED_MIN_VERSION_MARIA_DB
ir.async_create_issue(
hass,
DOMAIN,
"maria_db_range_index_regression",
is_fixable=False,
severity=ir.IssueSeverity.CRITICAL,
learn_more_url="https://jira.mariadb.org/browse/MDEV-25020",
translation_key="maria_db_range_index_regression",
translation_placeholders={"min_version": str(min_version)},
)
def setup_connection_for_dialect(
instance: Recorder,
dialect_name: str,
@@ -464,6 +518,18 @@ def setup_connection_for_dialect(
_fail_unsupported_version(
version or version_string, "MariaDB", MIN_VERSION_MARIA_DB
)
if version and (
(version < RECOMMENDED_MIN_VERSION_MARIA_DB)
or (MARIA_DB_106 <= version < RECOMMENDED_MIN_VERSION_MARIA_DB_106)
or (MARIA_DB_107 <= version < RECOMMENDED_MIN_VERSION_MARIA_DB_107)
or (MARIA_DB_108 <= version < RECOMMENDED_MIN_VERSION_MARIA_DB_108)
):
instance.hass.add_job(
_async_create_mariadb_range_index_regression_issue,
instance.hass,
version,
)
else:
if not version or version < MIN_VERSION_MYSQL:
_fail_unsupported_version(

View File

@@ -15,13 +15,18 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.json import JSON_DUMP
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import (
DataRateConverter,
DistanceConverter,
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter,
InformationConverter,
MassConverter,
PowerConverter,
PressureConverter,
SpeedConverter,
TemperatureConverter,
UnitlessRatioConverter,
VolumeConverter,
)
@@ -47,6 +52,24 @@ from .util import (
_LOGGER: logging.Logger = logging.getLogger(__package__)
UNIT_SCHEMA = vol.Schema(
{
vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS),
vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional("electric_current"): vol.In(ElectricCurrentConverter.VALID_UNITS),
vol.Optional("voltage"): vol.In(ElectricPotentialConverter.VALID_UNITS),
vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS),
vol.Optional("information"): vol.In(InformationConverter.VALID_UNITS),
vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS),
vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS),
vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS),
vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS),
vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS),
vol.Optional("unitless"): vol.In(UnitlessRatioConverter.VALID_UNITS),
vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS),
}
)
@callback
def async_setup(hass: HomeAssistant) -> None:
@@ -93,18 +116,7 @@ def _ws_get_statistic_during_period(
vol.Optional("types"): vol.All(
[vol.Any("max", "mean", "min", "change")], vol.Coerce(set)
),
vol.Optional("units"): vol.Schema(
{
vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS),
vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS),
vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS),
vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS),
vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS),
vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS),
vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS),
}
),
vol.Optional("units"): UNIT_SCHEMA,
**PERIOD_SCHEMA.schema,
}
)
@@ -211,18 +223,7 @@ async def ws_handle_get_statistics_during_period(
vol.Optional("end_time"): str,
vol.Optional("statistic_ids"): [str],
vol.Required("period"): vol.Any("5minute", "hour", "day", "week", "month"),
vol.Optional("units"): vol.Schema(
{
vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS),
vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS),
vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS),
vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS),
vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS),
vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS),
vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS),
}
),
vol.Optional("units"): UNIT_SCHEMA,
vol.Optional("types"): vol.All(
[vol.Any("last_reset", "max", "mean", "min", "state", "sum")],
vol.Coerce(set),

View File

@@ -257,7 +257,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = (
device_class=SensorDeviceClass.ENERGY,
name="Battery available energy",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.MEASUREMENT,
state_class=SensorStateClass.TOTAL,
),
RenaultSensorEntityDescription(
key="battery_temperature",

View File

@@ -9,14 +9,7 @@ import logging
from aiohttp import ClientConnectorError
import async_timeout
from reolink_aio.exceptions import (
ApiError,
InvalidContentTypeError,
LoginError,
NoDataError,
ReolinkError,
UnexpectedDataError,
)
from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
@@ -48,17 +41,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
try:
await host.async_init()
except UserNotAdmin as err:
except (UserNotAdmin, CredentialsInvalidError) as err:
await host.stop()
raise ConfigEntryAuthFailed(err) from err
except (
ClientConnectorError,
asyncio.TimeoutError,
ApiError,
InvalidContentTypeError,
LoginError,
NoDataError,
ReolinkException,
UnexpectedDataError,
ReolinkError,
) as err:
await host.stop()
raise ConfigEntryNotReady(
@@ -90,7 +80,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
update_interval=timedelta(seconds=DEVICE_UPDATE_INTERVAL),
)
# Fetch initial data so we have data when entities subscribe
await coordinator_device_config_update.async_config_entry_first_refresh()
try:
await coordinator_device_config_update.async_config_entry_first_refresh()
except ConfigEntryNotReady as err:
await host.stop()
raise err
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData(
host=host,

View File

@@ -9,7 +9,7 @@ from typing import Any
import aiohttp
from aiohttp.web import Request
from reolink_aio.api import Host
from reolink_aio.exceptions import ReolinkError
from reolink_aio.exceptions import ReolinkError, SubscriptionError
from homeassistant.components import webhook
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
@@ -76,7 +76,6 @@ class ReolinkHost:
raise ReolinkSetupException("Could not get mac address")
if not self._api.is_admin:
await self.stop()
raise UserNotAdmin(
f"User '{self._api.username}' has authorization level "
f"'{self._api.user_level}', only admin users can change camera settings"
@@ -182,22 +181,19 @@ class ReolinkHost:
)
return
if await self._api.subscribe(self._webhook_url):
_LOGGER.debug(
"Host %s: subscribed successfully to webhook %s",
self._api.host,
self._webhook_url,
)
else:
raise ReolinkWebhookException(
f"Host {self._api.host}: webhook subscription failed"
)
await self._api.subscribe(self._webhook_url)
_LOGGER.debug(
"Host %s: subscribed successfully to webhook %s",
self._api.host,
self._webhook_url,
)
async def renew(self) -> None:
"""Renew the subscription of motion events (lease time is 15 minutes)."""
try:
await self._renew()
except ReolinkWebhookException as err:
except SubscriptionError as err:
if not self._lost_subscription:
self._lost_subscription = True
_LOGGER.error(
@@ -220,25 +216,33 @@ class ReolinkHost:
return
timer = self._api.renewtimer
_LOGGER.debug(
"Host %s:%s should renew subscription in: %i seconds",
self._api.host,
self._api.port,
timer,
)
if timer > SUBSCRIPTION_RENEW_THRESHOLD:
return
if timer > 0:
if await self._api.renew():
try:
await self._api.renew()
except SubscriptionError as err:
_LOGGER.debug(
"Host %s: error renewing Reolink subscription, "
"trying to subscribe again: %s",
self._api.host,
err,
)
else:
_LOGGER.debug(
"Host %s successfully renewed Reolink subscription", self._api.host
)
return
_LOGGER.debug(
"Host %s: error renewing Reolink subscription, "
"trying to subscribe again",
self._api.host,
)
if not await self._api.subscribe(self._webhook_url):
raise ReolinkWebhookException(
f"Host {self._api.host}: webhook re-subscription failed"
)
await self._api.subscribe(self._webhook_url)
_LOGGER.debug(
"Host %s: Reolink re-subscription successful after it was expired",
self._api.host,
@@ -246,7 +250,7 @@ class ReolinkHost:
async def register_webhook(self) -> None:
"""Register the webhook for motion events."""
self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}"
self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}_ONVIF"
event_id = self.webhook_id
webhook.async_register(

View File

@@ -3,7 +3,7 @@
"name": "Reolink IP NVR/camera",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/reolink",
"requirements": ["reolink-aio==0.3.0"],
"requirements": ["reolink-aio==0.3.2"],
"dependencies": ["webhook"],
"codeowners": ["@starkillerOG"],
"iot_class": "local_polling",

View File

@@ -8,7 +8,7 @@
"samsungctl[websocket]==0.7.1",
"samsungtvws[async,encrypted]==2.5.0",
"wakeonlan==2.1.0",
"async-upnp-client==0.33.0"
"async-upnp-client==0.33.1"
],
"ssdp": [
{

View File

@@ -9,6 +9,7 @@ from datetime import date, datetime, timedelta, timezone
from decimal import Decimal, InvalidOperation as DecimalInvalidOperation
import logging
from math import ceil, floor, log10
import re
from typing import Any, Final, cast, final
from homeassistant.config_entries import ConfigEntry
@@ -84,6 +85,8 @@ _LOGGER: Final = logging.getLogger(__name__)
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
NEGATIVE_ZERO_PATTERN = re.compile(r"^-(0\.?0*)$")
SCAN_INTERVAL: Final = timedelta(seconds=30)
__all__ = [
@@ -596,21 +599,22 @@ class SensorEntity(Entity):
f"({type(value)})"
) from err
# This should raise in Home Assistant Core 2023.4
self._invalid_numeric_value_reported = True
report_issue = self._suggest_report_issue()
_LOGGER.warning(
"Sensor %s has device class %s, state class %s and unit %s "
"thus indicating it has a numeric value; however, it has the "
"non-numeric value: %s (%s); Please update your configuration "
"if your entity is manually configured, otherwise %s",
self.entity_id,
device_class,
state_class,
unit_of_measurement,
value,
type(value),
report_issue,
)
if not self._invalid_numeric_value_reported:
self._invalid_numeric_value_reported = True
report_issue = self._suggest_report_issue()
_LOGGER.warning(
"Sensor %s has device class %s, state class %s and unit %s "
"thus indicating it has a numeric value; however, it has the "
"non-numeric value: %s (%s); Please update your configuration "
"if your entity is manually configured, otherwise %s",
self.entity_id,
device_class,
state_class,
unit_of_measurement,
value,
type(value),
report_issue,
)
return value
else:
numerical_value = value
@@ -647,8 +651,14 @@ class SensorEntity(Entity):
unit_of_measurement,
)
value = f"{converted_numerical_value:.{precision}f}"
# This can be replaced with adding the z option when we drop support for
# Python 3.10
value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value)
elif precision is not None:
value = f"{numerical_value:.{precision}f}"
# This can be replaced with adding the z option when we drop support for
# Python 3.10
value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value)
# Validate unit of measurement used for sensors with a device class
if (
@@ -665,6 +675,7 @@ class SensorEntity(Entity):
(
"Entity %s (%s) is using native unit of measurement '%s' which "
"is not a valid unit for the device class ('%s') it is using; "
"expected one of %s; "
"Please update your configuration if your entity is manually "
"configured, otherwise %s"
),
@@ -672,6 +683,7 @@ class SensorEntity(Entity):
type(self),
native_unit_of_measurement,
device_class,
[str(unit) if unit else "no unit of measurement" for unit in units],
report_issue,
)

View File

@@ -513,7 +513,7 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass | None]
SensorDeviceClass.DATA_SIZE: set(SensorStateClass),
SensorDeviceClass.DATE: set(),
SensorDeviceClass.DISTANCE: set(SensorStateClass),
SensorDeviceClass.DURATION: set(),
SensorDeviceClass.DURATION: set(SensorStateClass),
SensorDeviceClass.ENERGY: {
SensorStateClass.TOTAL,
SensorStateClass.TOTAL_INCREASING,
@@ -535,10 +535,7 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass | None]
SensorDeviceClass.PM25: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.POWER_FACTOR: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.POWER: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.PRECIPITATION: {
SensorStateClass.TOTAL,
SensorStateClass.TOTAL_INCREASING,
},
SensorDeviceClass.PRECIPITATION: set(SensorStateClass),
SensorDeviceClass.PRECIPITATION_INTENSITY: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.PRESSURE: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.REACTIVE_POWER: {SensorStateClass.MEASUREMENT},

View File

@@ -3,7 +3,7 @@
"name": "Shelly",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==5.3.0"],
"requirements": ["aioshelly==5.3.1"],
"dependencies": ["bluetooth", "http"],
"zeroconf": [
{

View File

@@ -202,7 +202,9 @@ async def async_build_source_set(hass: HomeAssistant) -> set[IPv4Address | IPv6A
return {
source_ip
for source_ip in await network.async_get_enabled_source_ips(hass)
if not source_ip.is_loopback and not source_ip.is_global
if not source_ip.is_loopback
and not source_ip.is_global
and (source_ip.version == 6 and source_ip.scope_id or source_ip.version == 4)
}

View File

@@ -2,7 +2,7 @@
"domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/ssdp",
"requirements": ["async-upnp-client==0.33.0"],
"requirements": ["async-upnp-client==0.33.1"],
"dependencies": ["network"],
"after_dependencies": ["zeroconf"],
"codeowners": [],

View File

@@ -2,7 +2,7 @@
"domain": "synology_dsm",
"name": "Synology DSM",
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"requirements": ["py-synologydsm-api==2.0.2"],
"requirements": ["py-synologydsm-api==2.1.1"],
"codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
"config_flow": true,
"ssdp": [

View File

@@ -3,7 +3,7 @@
"domain": "tibber",
"name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber",
"requirements": ["pyTibber==0.26.11"],
"requirements": ["pyTibber==0.26.12"],
"codeowners": ["@danielhiversen"],
"quality_scale": "silver",
"config_flow": true,

View File

@@ -89,9 +89,9 @@ SENSOR_DESCRIPTIONS_FAN: tuple[TradfriSensorEntityDescription, ...] = (
TradfriSensorEntityDescription(
key="aqi",
name="air quality",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
icon="mdi:air-filter",
value=_get_air_quality,
),
TradfriSensorEntityDescription(

View File

@@ -3,7 +3,7 @@
"name": "UPnP/IGD",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/upnp",
"requirements": ["async-upnp-client==0.33.0", "getmac==0.8.2"],
"requirements": ["async-upnp-client==0.33.1", "getmac==0.8.2"],
"dependencies": ["network", "ssdp"],
"codeowners": ["@StevenLooman"],
"ssdp": [

View File

@@ -17,7 +17,7 @@ button:
description: >-
Name of the button to press. Known possible values are
LEFT, RIGHT, DOWN, UP, HOME, MENU, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT,
MUTE, RED, GREEN, BLUE, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN,
MUTE, RED, GREEN, BLUE, YELLOW, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN,
PLAY, PAUSE, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
required: true
example: "LEFT"

View File

@@ -161,12 +161,14 @@ def _state_diff(
additions[COMPRESSED_STATE_CONTEXT]["id"] = new_state.context.id
else:
additions[COMPRESSED_STATE_CONTEXT] = new_state.context.id
old_attributes = old_state.attributes
for key, value in new_state.attributes.items():
if old_attributes.get(key) != value:
additions.setdefault(COMPRESSED_STATE_ATTRIBUTES, {})[key] = value
if removed := set(old_attributes).difference(new_state.attributes):
diff[STATE_DIFF_REMOVALS] = {COMPRESSED_STATE_ATTRIBUTES: removed}
if (old_attributes := old_state.attributes) != (
new_attributes := new_state.attributes
):
for key, value in new_attributes.items():
if old_attributes.get(key) != value:
additions.setdefault(COMPRESSED_STATE_ATTRIBUTES, {})[key] = value
if removed := set(old_attributes).difference(new_attributes):
diff[STATE_DIFF_REMOVALS] = {COMPRESSED_STATE_ATTRIBUTES: removed}
return {ENTITY_EVENT_CHANGE: {new_state.entity_id: diff}}

View File

@@ -55,12 +55,3 @@ TRANSLATION_KEY_MAPPING = {
"zone_LINK_CONTROL": "zone_link_control",
"zone_LINK_AUDIO_DELAY": "zone_link_audio_delay",
}
ZONE_SLEEP_STATE_MAPPING = {
"off": "off",
"30 min": "30_min",
"60 min": "60_min",
"90 min": "90_min",
"120 min": "120_min",
}
STATE_ZONE_SLEEP_MAPPING = {val: key for key, val in ZONE_SLEEP_STATE_MAPPING.items()}

View File

@@ -3,7 +3,7 @@
"name": "MusicCast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast",
"requirements": ["aiomusiccast==0.14.4"],
"requirements": ["aiomusiccast==0.14.7"],
"ssdp": [
{
"manufacturer": "Yamaha Corporation"

View File

@@ -9,11 +9,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN, MusicCastCapabilityEntity, MusicCastDataUpdateCoordinator
from .const import (
STATE_ZONE_SLEEP_MAPPING,
TRANSLATION_KEY_MAPPING,
ZONE_SLEEP_STATE_MAPPING,
)
from .const import TRANSLATION_KEY_MAPPING
async def async_setup_entry(
@@ -48,10 +44,6 @@ class SelectableCapapility(MusicCastCapabilityEntity, SelectEntity):
async def async_select_option(self, option: str) -> None:
"""Select the given option."""
value = {val: key for key, val in self.capability.options.items()}[option]
# If the translation key is "zone_sleep", we need to translate
# Home Assistant state back to the MusicCast value
if self.translation_key == "zone_sleep":
value = STATE_ZONE_SLEEP_MAPPING[value]
await self.capability.set(value)
@property
@@ -62,20 +54,9 @@ class SelectableCapapility(MusicCastCapabilityEntity, SelectEntity):
@property
def options(self) -> list[str]:
"""Return the list possible options."""
# If the translation key is "zone_sleep", we need to translate
# the options to make them compatible with Home Assistant
if self.translation_key == "zone_sleep":
return list(STATE_ZONE_SLEEP_MAPPING)
return list(self.capability.options.values())
@property
def current_option(self) -> str | None:
"""Return the currently selected option."""
# If the translation key is "zone_sleep", we need to translate
# the value to make it compatible with Home Assistant
if (
value := self.capability.current
) is not None and self.translation_key == "zone_sleep":
return ZONE_SLEEP_STATE_MAPPING[value]
return value
return self.capability.options.get(self.capability.current)

View File

@@ -2,7 +2,7 @@
"domain": "yeelight",
"name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/yeelight",
"requirements": ["yeelight==0.7.10", "async-upnp-client==0.33.0"],
"requirements": ["yeelight==0.7.10", "async-upnp-client==0.33.1"],
"codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"],
"config_flow": true,
"dependencies": ["network"],

View File

@@ -37,6 +37,7 @@ DECONZ_DOMAIN = "deconz"
FORMATION_STRATEGY = "formation_strategy"
FORMATION_FORM_NEW_NETWORK = "form_new_network"
FORMATION_FORM_INITIAL_NETWORK = "form_initial_network"
FORMATION_REUSE_SETTINGS = "reuse_settings"
FORMATION_CHOOSE_AUTOMATIC_BACKUP = "choose_automatic_backup"
FORMATION_UPLOAD_MANUAL_BACKUP = "upload_manual_backup"
@@ -270,8 +271,21 @@ class BaseZhaFlow(FlowHandler):
strategies.append(FORMATION_REUSE_SETTINGS)
strategies.append(FORMATION_UPLOAD_MANUAL_BACKUP)
strategies.append(FORMATION_FORM_NEW_NETWORK)
# Do not show "erase network settings" if there are none to erase
if self._radio_mgr.current_settings is None:
strategies.append(FORMATION_FORM_INITIAL_NETWORK)
else:
strategies.append(FORMATION_FORM_NEW_NETWORK)
# Automatically form a new network if we're onboarding with a brand new radio
if not onboarding.async_is_onboarded(self.hass) and set(strategies) == {
FORMATION_UPLOAD_MANUAL_BACKUP,
FORMATION_FORM_INITIAL_NETWORK,
}:
return await self.async_step_form_initial_network()
# Otherwise, let the user choose
return self.async_show_menu(
step_id="choose_formation_strategy",
menu_options=strategies,
@@ -283,6 +297,13 @@ class BaseZhaFlow(FlowHandler):
"""Reuse the existing network settings on the stick."""
return await self._async_create_radio_entry()
async def async_step_form_initial_network(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Form an initial network."""
# This step exists only for translations, it does nothing new
return await self.async_step_form_new_network(user_input)
async def async_step_form_new_network(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -439,7 +460,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
return self.async_abort(reason="single_instance_allowed")
# Without confirmation, discovery can automatically progress into parts of the
# config flow logic that interacts with hardware!
# config flow logic that interacts with hardware.
if user_input is not None or not onboarding.async_is_onboarded(self.hass):
# Probe the radio type if we don't have one yet
if (

View File

@@ -224,7 +224,8 @@ class InovelliConfigEntityChannel(ZigbeeChannel):
"switch_type": False,
"button_delay": False,
"smart_bulb_mode": False,
"double_tap_up_for_full_brightness": True,
"double_tap_up_for_max_brightness": True,
"double_tap_down_for_min_brightness": True,
"led_color_when_on": True,
"led_color_when_off": True,
"led_intensity_when_on": True,

View File

@@ -4,10 +4,10 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [
"bellows==0.34.6",
"bellows==0.34.7",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.91",
"zha-quirks==0.0.92",
"zigpy-deconz==0.19.2",
"zigpy==0.53.0",
"zigpy-xbee==0.16.2",

View File

@@ -11,7 +11,7 @@ from typing import Any
import voluptuous as vol
from zigpy.application import ControllerApplication
import zigpy.backups
from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH
from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH, CONF_NWK_BACKUP_ENABLED
from zigpy.exceptions import NetworkNotFormed
from homeassistant import config_entries
@@ -126,6 +126,7 @@ class ZhaRadioManager:
app_config[CONF_DATABASE] = database_path
app_config[CONF_DEVICE] = self.device_settings
app_config[CONF_NWK_BACKUP_ENABLED] = False
app_config = self.radio_type.controller.SCHEMA(app_config)
app = await self.radio_type.controller.new(
@@ -206,6 +207,7 @@ class ZhaRadioManager:
# The list of backups will always exist
self.backups = app.backups.backups.copy()
self.backups.sort(reverse=True, key=lambda b: b.backup_time)
return backup

View File

@@ -511,7 +511,7 @@ class PolledSmartEnergySummation(SmartEnergySummation):
models={"ZLinky_TIC"},
)
class Tier1SmartEnergySummation(
SmartEnergySummation, id_suffix="tier1_summation_delivered"
PolledSmartEnergySummation, id_suffix="tier1_summation_delivered"
):
"""Tier 1 Smart Energy Metering summation sensor."""
@@ -524,7 +524,7 @@ class Tier1SmartEnergySummation(
models={"ZLinky_TIC"},
)
class Tier2SmartEnergySummation(
SmartEnergySummation, id_suffix="tier2_summation_delivered"
PolledSmartEnergySummation, id_suffix="tier2_summation_delivered"
):
"""Tier 2 Smart Energy Metering summation sensor."""
@@ -537,7 +537,7 @@ class Tier2SmartEnergySummation(
models={"ZLinky_TIC"},
)
class Tier3SmartEnergySummation(
SmartEnergySummation, id_suffix="tier3_summation_delivered"
PolledSmartEnergySummation, id_suffix="tier3_summation_delivered"
):
"""Tier 3 Smart Energy Metering summation sensor."""
@@ -550,7 +550,7 @@ class Tier3SmartEnergySummation(
models={"ZLinky_TIC"},
)
class Tier4SmartEnergySummation(
SmartEnergySummation, id_suffix="tier4_summation_delivered"
PolledSmartEnergySummation, id_suffix="tier4_summation_delivered"
):
"""Tier 4 Smart Energy Metering summation sensor."""
@@ -563,7 +563,7 @@ class Tier4SmartEnergySummation(
models={"ZLinky_TIC"},
)
class Tier5SmartEnergySummation(
SmartEnergySummation, id_suffix="tier5_summation_delivered"
PolledSmartEnergySummation, id_suffix="tier5_summation_delivered"
):
"""Tier 5 Smart Energy Metering summation sensor."""
@@ -576,7 +576,7 @@ class Tier5SmartEnergySummation(
models={"ZLinky_TIC"},
)
class Tier6SmartEnergySummation(
SmartEnergySummation, id_suffix="tier6_summation_delivered"
PolledSmartEnergySummation, id_suffix="tier6_summation_delivered"
):
"""Tier 6 Smart Energy Metering summation sensor."""

View File

@@ -31,7 +31,8 @@
"title": "Network Formation",
"description": "Choose the network settings for your radio.",
"menu_options": {
"form_new_network": "Erase network settings and form a new network",
"form_new_network": "Erase network settings and create a new network",
"form_initial_network": "Create a network",
"reuse_settings": "Keep radio network settings",
"choose_automatic_backup": "Restore an automatic backup",
"upload_manual_backup": "Upload a manual backup"
@@ -86,11 +87,11 @@
},
"intent_migrate": {
"title": "Migrate to a new radio",
"description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?"
"description": "Before plugging in your new radio, your old radio needs to be reset. An automatic backup will be performed. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\n*Note: if you are migrating from a **ConBee/RaspBee**, make sure it is running firmware `0x26720700` or newer! Otherwise, some devices may not be controllable after migrating until they are power cycled.*\n\nDo you wish to continue?"
},
"instruct_unplug": {
"title": "Unplug your old radio",
"description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it."
"description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.\n\nYou can now plug in your new radio."
},
"choose_serial_port": {
"title": "[%key:component::zha::config::step::choose_serial_port::title%]",
@@ -120,6 +121,7 @@
"description": "[%key:component::zha::config::step::choose_formation_strategy::description%]",
"menu_options": {
"form_new_network": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::form_new_network%]",
"form_initial_network": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::form_initial_network%]",
"reuse_settings": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::reuse_settings%]",
"choose_automatic_backup": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::choose_automatic_backup%]",
"upload_manual_backup": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::upload_manual_backup%]"

View File

@@ -372,14 +372,26 @@ class InovelliSmartBulbMode(ZHASwitchConfigurationEntity, id_suffix="smart_bulb_
channel_names=CHANNEL_INOVELLI,
)
class InovelliDoubleTapForFullBrightness(
ZHASwitchConfigurationEntity, id_suffix="double_tap_up_for_full_brightness"
ZHASwitchConfigurationEntity, id_suffix="double_tap_up_for_max_brightness"
):
"""Inovelli double tap for full brightness control."""
_zcl_attribute: str = "double_tap_up_for_full_brightness"
_zcl_attribute: str = "double_tap_up_for_max_brightness"
_attr_name: str = "Double tap full brightness"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliDoubleTapForMinBrightness(
ZHASwitchConfigurationEntity, id_suffix="double_tap_down_for_min_brightness"
):
"""Inovelli double tap down for minimum brightness control."""
_zcl_attribute: str = "double_tap_down_for_min_brightness"
_attr_name: str = "Double tap minimum brightness"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)

View File

@@ -22,7 +22,8 @@
"description": "Choose the network settings for your radio.",
"menu_options": {
"choose_automatic_backup": "Restore an automatic backup",
"form_new_network": "Erase network settings and form a new network",
"form_initial_network": "Create a network",
"form_new_network": "Erase network settings and create a new network",
"reuse_settings": "Keep radio network settings",
"upload_manual_backup": "Upload a manual backup"
},
@@ -174,7 +175,8 @@
"description": "Choose the network settings for your radio.",
"menu_options": {
"choose_automatic_backup": "Restore an automatic backup",
"form_new_network": "Erase network settings and form a new network",
"form_initial_network": "Create a network",
"form_new_network": "Erase network settings and create a new network",
"reuse_settings": "Keep radio network settings",
"upload_manual_backup": "Upload a manual backup"
},
@@ -192,11 +194,11 @@
"title": "Reconfigure ZHA"
},
"instruct_unplug": {
"description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.",
"description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.\n\nYou can now plug in your new radio.",
"title": "Unplug your old radio"
},
"intent_migrate": {
"description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?",
"description": "Before plugging in your new radio, your old radio needs to be reset. An automatic backup will be performed. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\n*Note: if you are migrating from a **ConBee/RaspBee**, make sure it is running firmware `0x26720700` or newer! Otherwise, some devices may not be controllable after migrating until they are power cycled.*\n\nDo you wish to continue?",
"title": "Migrate to a new radio"
},
"manual_pick_radio_type": {

View File

@@ -1,7 +1,7 @@
"""Representation of a sirenBinary."""
from typing import Any
from homeassistant.components.siren import SirenEntity
from homeassistant.components.siren import SirenEntity, SirenEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -41,6 +41,13 @@ async def async_setup_entry(
class ZWaveMeSiren(ZWaveMeEntity, SirenEntity):
"""Representation of a ZWaveMe siren."""
def __init__(self, controller, device):
"""Initialize the device."""
super().__init__(controller, device)
self._attr_supported_features = (
SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF
)
@property
def is_on(self) -> bool:
"""Return the state of the siren."""

View File

@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "0.dev0"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)

View File

@@ -2007,7 +2007,7 @@
"name": "Google Domains"
},
"google_mail": {
"integration_type": "device",
"integration_type": "service",
"config_flow": true,
"iot_class": "cloud_polling",
"name": "Google Mail"

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